From ed675c8e01bfff9abe52bda2cfb6a5d55288e293 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Mon, 27 Jul 2026 12:13:55 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20#135:=20Stored=20XSS=20=C3=BCber=20XSLT?= =?UTF-8?q?=20in=20hochgeladenen=20XML-Belegen=20behoben?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Changes.md | 7 +++ public/index.php | 2 + src/Controllers/BelegController.php | 7 ++- tests/67_BelegXmlXsltXssTest.php | 76 +++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tests/67_BelegXmlXsltXssTest.php diff --git a/Changes.md b/Changes.md index 4147f3e..111093e 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,13 @@ --- +## 2026-07-27 – Fix: Stored XSS über XSLT in hochgeladenen XML-Belegen (Issue #135) + +- `BelegController::serveFile()` liefert Belege mit dem in der DB hinterlegten `mime_type` und `Content-Disposition: inline` unverändert an den Browser aus. Da für Belege `application/xml`/`text/xml` als MIME-Typ erlaubt ist, konnte eine hochgeladene XML-Datei mit eingebettetem ``/`xsl:stylesheet` beim Öffnen über den „Datei öffnen“-Link (`target="_blank"`, kein `download`-Attribut) direkt im Browser (v.a. Firefox) per XSLT-Transformation zu ausführbarem ` + + + + XML; + file_put_contents($absPath, $xmlPayload); + + $belegId = \Steuerkram\Models\Beleg::create([ + 'beleg_nr' => $belegNr, + 'filename' => 'rechnung.xml', + 'storage_path' => $storagePath, + 'mime_type' => 'text/xml', + 'file_hash' => hash('sha256', $xmlPayload), + 'beleg_datum' => date('Y-m-d'), + 'rechnungs_nr' => null, + 'betrag_brutto' => 100.00, + 'belegtyp' => 'Eingangsrechnung', + 'notes' => 'XSLT-XSS-Test', + ]); + + $controller = new \Steuerkram\Controllers\BelegController(); + + ob_start(); + $controller->serveFile(['id' => (string)$belegId]); + ob_get_clean(); + + $headers = headers_list(); + $disposition = ''; + foreach ($headers as $header) { + if (stripos($header, 'Content-Disposition:') === 0) { + $disposition = $header; + } + } + + $this->assertNotEmpty( + $disposition, + 'Es wurde ein Content-Disposition-Header gesetzt' + ); + $this->assert( + stripos($disposition, 'attachment') !== false, + "XML-Beleg wird als attachment statt inline ausgeliefert (erhalten: {$disposition})" + ); + + @unlink($absPath); + } +}