Fix #135: Stored XSS über XSLT in hochgeladenen XML-Belegen behoben
This commit is contained in:
parent
8000225a05
commit
ed675c8e01
4 changed files with 91 additions and 1 deletions
|
|
@ -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 `<?xml-stylesheet?>`/`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 `<script>` im Sicherheitskontext der Anwendung führen (Stored-XSS, CWE-79) – inklusive Zugriff auf das im `<meta>`-Tag liegende CSRF-Token (vgl. #132/#133).
|
||||
- **`src/Controllers/BelegController.php`:** `serveFile()` liefert XML-Belege (`application/xml`/`text/xml`) jetzt mit `Content-Disposition: attachment` statt `inline` aus, sodass der Browser die Datei herunterlädt statt sie zu navigieren/rendern; andere MIME-Typen bleiben unverändert `inline`.
|
||||
- **`public/index.php`:** Globaler `X-Content-Type-Options: nosniff`-Header für alle Responses als zusätzliche Absicherung gegen MIME-Sniffing.
|
||||
- **`tests/67_BelegXmlXsltXssTest.php`:** Neuer Test lädt einen XML-Beleg mit XSLT-Payload an und prüft, dass `serveFile()` den `Content-Disposition`-Header auf `attachment` statt `inline` setzt.
|
||||
|
||||
## 2026-07-27 – Fix: DOM-XSS in der CSV/XLSX-Vorschau beim Bankimport (Issue #133)
|
||||
|
||||
- `BankController::detectFormat()` liefert die aus einer hochgeladenen Bank-Umsatzdatei geparste `header_row`/`first_data` unverändert als JSON zurück; `src/Views/bank/upload.php` rendert diese Werte per `innerHTML` in ein `title="..."`-Attribut. Die dortige `esc()`-Funktion escapte bisher nur `&`, `<` und `>`, nicht aber `"`. Ein Header-/Zellwert wie `Spalte" onmouseover="alert(document.cookie)` konnte damit aus dem Attribut ausbrechen und beliebigen JavaScript-Code im Kontext des eingeloggten Nutzers ausführen (CWE-79), z.B. um das im `<meta>`-Tag liegende CSRF-Token auszulesen. Bank-Exportdateien stammen typischerweise von Dritten (Bank, per E-Mail zugeschickter Kontoauszug) und sind damit ein realistischer, angreiferkontrollierter Eingabekanal.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ define('BASE_DIR', dirname(__DIR__));
|
|||
|
||||
require BASE_DIR . '/vendor/autoload.php';
|
||||
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
// Cache-Buster für statische Assets: Git-Commit-Hash aus Datei lesen
|
||||
$_assetVersionFile = BASE_DIR . '/public/static/.asset-version';
|
||||
define('ASSET_VERSION', file_exists($_assetVersionFile) ? trim(file_get_contents($_assetVersionFile)) : 'dev');
|
||||
|
|
|
|||
|
|
@ -323,8 +323,13 @@ class BelegController extends BaseController
|
|||
}
|
||||
if (!file_exists($realPath)) { http_response_code(404); return; }
|
||||
|
||||
// XML kann eingebettete XSLT-Stylesheets enthalten, die bei Inline-Anzeige
|
||||
// im Browser (v.a. Firefox) als Stored-XSS ausgeführt würden -> Download erzwingen.
|
||||
$isXml = in_array($beleg['mime_type'], ['application/xml', 'text/xml'], true);
|
||||
$disposition = $isXml ? 'attachment' : 'inline';
|
||||
|
||||
header('Content-Type: ' . $beleg['mime_type']);
|
||||
header('Content-Disposition: inline; filename="' . $beleg['filename'] . '"');
|
||||
header('Content-Disposition: ' . $disposition . '; filename="' . $beleg['filename'] . '"');
|
||||
header('Content-Length: ' . filesize($realPath));
|
||||
readfile($realPath);
|
||||
}
|
||||
|
|
|
|||
76
tests/67_BelegXmlXsltXssTest.php
Normal file
76
tests/67_BelegXmlXsltXssTest.php
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Test 67: Stored XSS über XSLT in hochgeladenen XML-Belegen ist geschlossen
|
||||
* (BelegController::serveFile liefert XML-Belege als Download statt inline aus).
|
||||
*/
|
||||
class BelegXmlXsltXssTest extends TestCase
|
||||
{
|
||||
protected string $testName = 'Test 67: XML-Belege werden als Attachment statt inline ausgeliefert';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$belegNr = \Steuerkram\Models\Beleg::generateBelegNr();
|
||||
$year = date('Y');
|
||||
$relDir = "{$year}/{$belegNr}";
|
||||
$absDir = \Steuerkram\Config::get('upload_dir') . "/{$relDir}";
|
||||
if (!is_dir($absDir)) {
|
||||
mkdir($absDir, 0755, true);
|
||||
}
|
||||
$storagePath = "{$relDir}/original.xml";
|
||||
$absPath = \Steuerkram\Config::get('upload_dir') . "/{$storagePath}";
|
||||
|
||||
$xmlPayload = <<<'XML'
|
||||
<?xml version="1.0"?>
|
||||
<?xml-stylesheet type="text/xsl" href="#stylesheet"?>
|
||||
<doc>
|
||||
<xsl:stylesheet id="stylesheet" version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:template match="/">
|
||||
<html><body><script>alert(document.domain)</script></body></html>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
||||
</doc>
|
||||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue