89 lines
3.6 KiB
PHP
89 lines
3.6 KiB
PHP
<?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 = $this->makeTempDir(\Steuerkram\Config::get('upload_dir') . "/{$relDir}");
|
||
$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]);
|
||
$body = (string)ob_get_clean();
|
||
|
||
// Der Beleg muss überhaupt ausgeliefert werden (kein 403 durch den
|
||
// Pfad-Check, kein 404) – sonst sagt die Disposition-Prüfung nichts aus.
|
||
$this->assert(
|
||
str_contains($body, 'xsl:stylesheet'),
|
||
'serveFile() liefert den XML-Beleg aus (kein 403/404)'
|
||
);
|
||
|
||
// headers_list() ist unter der CLI-SAPI immer leer, der real gesetzte
|
||
// Header lässt sich hier also nicht auslesen. Geprüft wird deshalb die
|
||
// Auslieferungslogik in serveFile() selbst.
|
||
$controllerFile = __DIR__ . '/../src/Controllers/BelegController.php';
|
||
$quelltext = (string)file_get_contents($controllerFile);
|
||
|
||
$start = strpos($quelltext, 'function serveFile');
|
||
$this->assert($start !== false, 'Methode serveFile() ist im BelegController vorhanden');
|
||
|
||
$rest = substr($quelltext, (int)$start);
|
||
$laenge = preg_match('/\n (?:public|private|protected) function /', $rest, $treffer, PREG_OFFSET_CAPTURE)
|
||
? $treffer[0][1]
|
||
: strlen($rest);
|
||
$serveFile = substr($rest, 0, $laenge);
|
||
|
||
$this->assert(
|
||
str_contains($serveFile, "'application/xml'") && str_contains($serveFile, "'text/xml'"),
|
||
'serveFile() erkennt XML-MIME-Typen gesondert'
|
||
);
|
||
$this->assert(
|
||
(bool)preg_match("/\?\s*'attachment'\s*:\s*'inline'/", $serveFile),
|
||
'XML wird als attachment ausgeliefert, alles andere weiterhin inline'
|
||
);
|
||
$this->assert(
|
||
(bool)preg_match("/Content-Disposition:\s*'\s*\.\s*\\\$disposition/", $serveFile),
|
||
'Der Content-Disposition-Header nutzt die ermittelte Disposition (nicht fest inline)'
|
||
);
|
||
}
|
||
}
|