VYPR
Critical severity9.8NVD Advisory· Published Dec 12, 2024· Updated Apr 15, 2026

CVE-2024-55875

CVE-2024-55875

Description

http4k is a functional toolkit for Kotlin HTTP applications. Prior to version 5.41.0.0, there is a potential XXE (XML External Entity Injection) vulnerability when http4k handling malicious XML contents within requests, which might allow attackers to read local sensitive information on server, trigger Server-side Request Forgery and even execute code under some circumstances. Version 5.41.0.0 contains a patch for the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.http4k:http4k-format-xmlMaven
>= 5.0.0.0, < 5.41.0.05.41.0.0
org.http4k:http4k-format-xmlMaven
< 4.50.0.04.50.0.0

Patches

2
35297adc6d6a

Merge commit from fork

https://github.com/http4k/http4kIvan SanchezDec 12, 2024via ghsa
2 files changed · +139 6
  • core/format/xml/src/main/kotlin/org/http4k/format/Xml.kt+17 6 modified
    @@ -19,6 +19,7 @@ import org.w3c.dom.Document
     import java.io.InputStream
     import java.io.StringWriter
     import java.nio.ByteBuffer
    +import javax.xml.XMLConstants
     import javax.xml.parsers.DocumentBuilderFactory
     import javax.xml.transform.TransformerFactory
     import javax.xml.transform.dom.DOMSource
    @@ -39,9 +40,10 @@ object Xml : AutoMarshallingXml() {
         @JvmName("stringAsXmlToJsonElement")
         fun asXmlToJsonElement(input: String): JsonElement = input.asXmlToJsonElement()
     
    -    fun String.asXmlDocument(): Document =
    +    fun String.asXmlDocument(config: XmlParsingConfig = defaultXmlParsingConfig): Document =
             DocumentBuilderFactory
                 .newInstance()
    +            .apply(config)
                 .newDocumentBuilder()
                 .parse(byteInputStream())
     
    @@ -63,14 +65,15 @@ object Xml : AutoMarshallingXml() {
         /**
          * Convenience function to write the object as XML to the message body and set the content type.
          */
    -    fun <IN : Any> BiDiLensSpec<IN, String>.xml() = map({ it.asXmlDocument() }, { it.asXmlString() })
    +    fun <IN : Any> BiDiLensSpec<IN, String>.xml(config: XmlParsingConfig = defaultXmlParsingConfig) = map({ it.asXmlDocument(config) }, { it.asXmlString() })
     
    -    fun asBiDiMapping() =
    -        BiDiMapping<String, Document>({ it.asXmlDocument() }, { it.asXmlString() })
    +    fun asBiDiMapping(config: XmlParsingConfig = defaultXmlParsingConfig) =
    +        BiDiMapping<String, Document>({ it.asXmlDocument(config) }, { it.asXmlString() })
     
         fun Body.Companion.xml(
             description: String? = null,
    -        contentNegotiation: ContentNegotiation = ContentNegotiation.None
    +        contentNegotiation: ContentNegotiation = ContentNegotiation.None,
    +        config: XmlParsingConfig = defaultXmlParsingConfig
         ): BiDiBodyLensSpec<Document> =
             httpBodyRoot(
                 listOf(Meta(true, "body", ObjectParam, "body", description, emptyMap())),
    @@ -79,5 +82,13 @@ object Xml : AutoMarshallingXml() {
             )
                 .map(Body::payload) { Body(it) }
                 .map(ByteBuffer::asString, String::asByteBuffer)
    -            .map({ it.asXmlDocument() }, { it.asXmlString() })
    +            .map({ it.asXmlDocument(config) }, { it.asXmlString() })
    +}
    +
    +typealias XmlParsingConfig = DocumentBuilderFactory.() -> Unit
    +
    +val defaultXmlParsingConfig: XmlParsingConfig = {
    +    isExpandEntityReferences = false
    +    setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "")
    +    setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "")
     }
    
  • core/format/xml/src/test/kotlin/org/http4k/format/XmlSecurityTest.kt+122 0 added
    @@ -0,0 +1,122 @@
    +package org.http4k.format
    +
    +import com.natpryce.hamkrest.assertion.assertThat
    +import com.natpryce.hamkrest.equalTo
    +import org.http4k.core.Body
    +import org.http4k.core.ContentType.Companion.APPLICATION_XML
    +import org.http4k.core.HttpHandler
    +import org.http4k.core.Method
    +import org.http4k.core.Request
    +import org.http4k.core.Response
    +import org.http4k.core.Status.Companion.BAD_REQUEST
    +import org.http4k.core.Status.Companion.OK
    +import org.http4k.format.Xml.asXmlString
    +import org.http4k.format.Xml.xml
    +import org.http4k.lens.contentType
    +import org.http4k.server.ApacheServer
    +import org.http4k.server.asServer
    +import org.http4k.util.PortBasedTest
    +import org.junit.jupiter.api.Test
    +import org.w3c.dom.Document
    +import java.util.concurrent.atomic.AtomicBoolean
    +
    +class XmlSecurityTest : PortBasedTest {
    +
    +    @Test
    +    fun `does not expand external entity`() {
    +        val websiteAccessed = AtomicBoolean(false)
    +
    +        val maliciousWebsite = { _: Request ->
    +            websiteAccessed.set(true);
    +            Response(OK)
    +        }.asServer(ApacheServer(0)).start()
    +
    +        val requestBody =
    +            """<?xml version="1.0" encoding="UTF-8"?>
    +                <!DOCTYPE root [<!ENTITY xxe SYSTEM "http://localhost:${maliciousWebsite.port()}">]>
    +                <root>&xxe;</root>
    +            """.trimIndent()
    +
    +        val xmlLens = Body.xml().toLens()
    +
    +        val app: HttpHandler = { request ->
    +            try {
    +                val xmlDocument: Document = xmlLens(request)
    +                Response(OK).body(xmlDocument.asXmlString())
    +            } catch (e: Exception) {
    +                Response(BAD_REQUEST).body("Invalid XML: ${e.message}")
    +            }
    +        }
    +
    +        app(Request(Method.POST, "/").contentType(APPLICATION_XML).body(requestBody))
    +        assertThat(websiteAccessed.get(), equalTo(false))
    +    }
    +
    +    @Test
    +    fun `external schema is not loaded`() {
    +        val websiteAccessed = AtomicBoolean(false)
    +
    +        val maliciousWebsite = { _: Request ->
    +            websiteAccessed.set(true);
    +            Response(OK)
    +        }.asServer(ApacheServer(0)).start()
    +
    +        val requestBody = """
    +            <?xml version="1.0" encoding="UTF-8"?>
    +            <user xmlns:xsi="http://localhost:${maliciousWebsite.port()}"
    +                  xsi:noNamespaceSchemaLocation="http://localhost:${maliciousWebsite.port()}">
    +                <name>John Doe</name>
    +                <email>john@example.com</email>
    +                <age>30</age>
    +            </user>
    +        """.trimIndent()
    +
    +        val xmlLens = Body.xml().toLens()
    +
    +        val app: HttpHandler = { request ->
    +            try {
    +                val xmlDocument: Document = xmlLens(request)
    +                Response(OK).body(xmlDocument.asXmlString())
    +            } catch (e: Exception) {
    +                Response(BAD_REQUEST).body("Invalid XML: ${e.message}")
    +            }
    +        }
    +
    +        app(Request(Method.POST, "/").contentType(APPLICATION_XML).body(requestBody))
    +        assertThat(websiteAccessed.get(), equalTo(false))
    +    }
    +
    +    @Test
    +    fun `external dtd is not loaded`() {
    +        val websiteAccessed = AtomicBoolean(false)
    +
    +        val maliciousWebsite = { _: Request ->
    +            websiteAccessed.set(true);
    +            Response(OK)
    +        }.asServer(ApacheServer(0)).start()
    +
    +        val requestBody = """
    +            <?xml version="1.0" encoding="UTF-8"?>
    +            <!DOCTYPE note SYSTEM "http://localhost:${maliciousWebsite.port()}">
    +            <note>
    +                <to>Alice</to>
    +                <from>Bob</from>
    +                <message>Hello</message>
    +            </note>
    +        """.trimIndent()
    +
    +        val xmlLens = Body.xml().toLens()
    +
    +        val app: HttpHandler = { request ->
    +            try {
    +                val xmlDocument: Document = xmlLens(request)
    +                Response(OK).body(xmlDocument.asXmlString())
    +            } catch (e: Exception) {
    +                Response(BAD_REQUEST).body("Invalid XML: ${e.message}")
    +            }
    +        }
    +
    +        app(Request(Method.POST, "/").contentType(APPLICATION_XML).body(requestBody))
    +        assertThat(websiteAccessed.get(), equalTo(false))
    +    }
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.