🔒️ Fix XXE vulnerability and add test
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 4 Oct 2026 15:31:57 +0000 (17:31 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 4 Oct 2026 17:21:16 +0000 (19:21 +0200)
While the parser in its configuration was vulnerable to XXE, you could only
ever manipulate XML files that you inserted, and it was never possible to force
other Sone instances to re-export any data from your modified XML files.

build.gradle
src/test/kotlin/net/pterodactylus/sone/core/SoneParserTest.kt

index 938d645..066377c 100644 (file)
@@ -47,7 +47,7 @@ dependencies {
     implementation group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8'
     implementation group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '1.3.0-RC'
 
-    implementation group: 'net.pterodactylus', name: 'utils', version: '0.13.1'
+    implementation group: 'net.pterodactylus', name: 'utils', version: '0.13.2'
     implementation group: 'com.google.inject', name: 'guice', version: '7.0.0'
     implementation group: 'com.google.guava', name: 'guava', version: '33.3.1-jre'
     implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.18.0'
index 38419a2..5af592a 100644 (file)
@@ -2,6 +2,7 @@ package net.pterodactylus.sone.core
 
 import com.codahale.metrics.*
 import com.google.common.base.Optional.*
+import com.sun.net.httpserver.HttpServer
 import freenet.crypt.*
 import freenet.keys.InsertableClientSSK.*
 import net.pterodactylus.sone.data.*
@@ -14,8 +15,12 @@ import org.hamcrest.MatcherAssert.*
 import org.hamcrest.Matchers.*
 import org.mockito.Mockito.*
 import java.lang.System.*
+import java.net.InetAddress
+import java.net.InetSocketAddress
 import java.util.concurrent.TimeUnit.*
 import kotlin.test.*
+import org.junit.Rule
+import org.junit.rules.TemporaryFolder
 
 /**
  * Unit test for [SoneParser].
@@ -415,4 +420,50 @@ class SoneParserTest {
                assertThat(histogram.count, equalTo(1L))
        }
 
+       @Test
+       fun `custom entities will not cause files to be included`() {
+               val fileToRead = temporaryFolder.newFile()
+               fileToRead.writeText("secret")
+               val maliciousEntityXml = """<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE sone [<!ENTITY xxe SYSTEM "file://${fileToRead.absolutePath}">]>
+<sone>
+       <protocol-version>0</protocol-version>
+       <time>1407197508000</time>
+       <profile>
+               <first-name>xxe: &xxe;</first-name>
+       </profile>
+</sone>
+"""
+               assertThat(soneParser.parseSone(sone, maliciousEntityXml.byteInputStream())!!.profile.firstName, equalTo("xxe: "))
+       }
+
+       @Test
+       fun `custom entities will not cause URLs to be included`() {
+               val httpServer = HttpServer.create(InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0)
+               httpServer.createContext("/test") { exchange ->
+                       exchange.sendResponseHeaders(200, 6)
+                       exchange.responseBody.write("secret".encodeToByteArray())
+               }
+               httpServer.start()
+               try {
+                       val maliciousEntityXml = """<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE sone [<!ENTITY xxe SYSTEM "http://${httpServer.address.hostName}:${httpServer.address.port}/test">]>
+<sone>
+       <protocol-version>0</protocol-version>
+       <time>1407197508000</time>
+       <profile>
+               <first-name>xxe: &xxe;</first-name>
+       </profile>
+</sone>
+"""
+                       assertThat(soneParser.parseSone(sone, maliciousEntityXml.byteInputStream())!!.profile.firstName, equalTo("xxe: "))
+               } finally {
+                       httpServer.stop(0)
+               }
+       }
+
+       @Rule
+       @JvmField
+       val temporaryFolder: TemporaryFolder = TemporaryFolder()
+
 }