110 lines
6.9 KiB
PHP
110 lines
6.9 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Prüft den HTML-Bereinigungsfilter (sanitize_staff_html) gegen typische XSS-Angriffe.
|
|
* Wichtig, weil damit HTML-E-Mails fremder Absender im Mitarbeiterbereich angezeigt werden.
|
|
*/
|
|
|
|
putenv('DESKHUB_SQLITE_PATH=:memory:');
|
|
require_once dirname(__DIR__) . '/includes/bootstrap.php';
|
|
|
|
$failures = [];
|
|
$check = function (bool $ok, string $what) use (&$failures): void {
|
|
if (!$ok) $failures[] = $what;
|
|
};
|
|
|
|
/** Prüft, ob im Ergebnis ausführbares Markup stecken könnte. Gibt den Grund zurück oder null. */
|
|
$dangerous = function (string $out): ?string {
|
|
if (preg_match('/<\s*(script|iframe|object|embed|svg|math|form|input|style|link|meta|base)\b/i', $out)) return 'gefährliches Tag';
|
|
if (preg_match_all('/<([a-z0-9]+)((?:\s[^<>]*)?)>/i', $out, $m, PREG_SET_ORDER)) {
|
|
foreach ($m as $t) {
|
|
// Attributwerte entfernen, dann dürfen nur erlaubte Attributnamen übrig sein
|
|
$names = preg_replace('/"[^"]*"/', '', $t[2]);
|
|
if (preg_match_all('/\s([a-z-]+)=/i', (string) $names, $a)) {
|
|
foreach ($a[1] as $name) {
|
|
if (!in_array(strtolower($name), ['href','target','rel','src','alt','width','height','title','style','colspan','rowspan','align','valign','border','cellpadding','cellspacing'], true)) return "Attribut {$name}";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Übrig gebliebene "<" oder ">" außerhalb vollständiger, erlaubter Tags wären gefährlich
|
|
$stripped = preg_replace('/<\/?[a-z0-9]+(?:\s[^<>]*)?\/?>/i', '', $out);
|
|
if (str_contains((string) $stripped, '<') || str_contains((string) $stripped, '>')) return 'loses < oder >';
|
|
return null;
|
|
};
|
|
|
|
$payloads = [
|
|
'script' => '<script>alert(1)</script>Hallo',
|
|
'script verschachtelt' => '<scr<script>ipt>alert(1)</scr</script>ipt>',
|
|
'img onerror' => '<img src=x onerror=alert(1)>',
|
|
'img onerror quoted' => '<img src="x" onerror="alert(1)">',
|
|
'unterminiert' => 'Text <img src=x onerror=alert(1)',
|
|
'unterminiert svg' => '<svg onload=alert(1)',
|
|
'doppelt <' => '<<img src=x onerror=alert(1)//>',
|
|
'a javascript' => '<a href="javascript:alert(1)">x</a>',
|
|
'a javascript entity' => '<a href="javascript:alert(1)">x</a>',
|
|
'a javascript tab' => '<a href="java	script:alert(1)">x</a>',
|
|
'a data' => '<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">x</a>',
|
|
'a onclick' => '<a href="https://example.org" onclick="alert(1)">x</a>',
|
|
'a attribut ausbruch' => '<a href="https://example.org" onmouseover="alert(1)">x</a>',
|
|
'img svg data' => '<img src="data:image/svg+xml;base64,PHN2ZyBvbmxvYWQ9YWxlcnQoMSk+">',
|
|
'style expression' => '<div style="width:expression(alert(1))">x</div>',
|
|
'style url' => '<div style="background:url(https://evil.example/t.gif)">x</div>',
|
|
'style url entity' => '<div style="background:url(https://evil.example/t.gif)">x</div>',
|
|
'style position' => '<div style="position:fixed;top:0;left:0;width:100%;height:100%">Bitte Passwort eingeben</div>',
|
|
'style ausbruch' => '<div style="color:red" onmouseover="alert(1)">x</div>',
|
|
'iframe' => '<iframe src="https://evil.example"></iframe>',
|
|
'iframe srcdoc' => '<iframe srcdoc="<script>alert(1)</script>"></iframe>',
|
|
'form' => '<form action="https://evil.example"><input name=pw><button>Senden</button></form>',
|
|
'meta refresh' => '<meta http-equiv="refresh" content="0;url=https://evil.example">',
|
|
'base' => '<base href="https://evil.example/">',
|
|
'object' => '<object data="x"></object><embed src="x">',
|
|
'math' => '<math><mi xlink:href="javascript:alert(1)">x</mi></math>',
|
|
'kommentar' => '<!--><script>alert(1)</script>-->',
|
|
'bedingt' => '<!--[if gte IE 4]><script>alert(1)</script><![endif]-->',
|
|
'cdata' => '<![CDATA[<script>alert(1)</script>]]>',
|
|
'attribut ohne quotes' => '<img src=https://x.example/a.png onerror=alert(1)>',
|
|
'grossschreibung' => '<IMG SRC=x ONERROR=alert(1)><SCRIPT>alert(1)</SCRIPT>',
|
|
'null byte' => "<scr\0ipt>alert(1)</scr\0ipt>",
|
|
'tag mit newline' => "<img\nsrc=x\nonerror=alert(1)>",
|
|
'slash statt space' => '<img/src=x/onerror=alert(1)>',
|
|
'details ontoggle' => '<details open ontoggle=alert(1)>x</details>',
|
|
'td background' => '<table background="javascript:alert(1)"><tr><td>x</td></tr></table>',
|
|
];
|
|
foreach ($payloads as $name => $input) {
|
|
$out = sanitize_staff_html($input);
|
|
$why = $dangerous($out);
|
|
$check($why === null, "Angriff '{$name}' nicht entschärft ({$why}): {$out}");
|
|
}
|
|
|
|
// Konkrete Erwartungen
|
|
$out = sanitize_staff_html('Text <img src=x onerror=alert(1)');
|
|
$check(!str_contains($out, '<img') && str_contains($out, '<img'), 'Unterminiertes Tag muss als Text maskiert werden: ' . $out);
|
|
$out = sanitize_staff_html('<a href="javascript:alert(1)">x</a>');
|
|
$check(!str_contains($out, 'javascript') && str_contains($out, 'rel="noopener noreferrer"'), 'javascript-Link: ' . $out);
|
|
$out = sanitize_staff_html('<div style="position:fixed;color:red">x</div>');
|
|
$check(!str_contains($out, 'position') && str_contains($out, 'color: red'), 'position muss entfernt, color bleiben: ' . $out);
|
|
$out = sanitize_staff_html('<div style="background:url(https://evil.example/t.gif)">x</div>');
|
|
$check(!str_contains($out, 'url') && !str_contains($out, 'evil'), 'url() im Stil: ' . $out);
|
|
$out = sanitize_staff_html('<img src="http://tracker.example/p.gif">');
|
|
$check(!str_contains($out, 'tracker.example'), 'http-Bild muss ersetzt werden: ' . $out);
|
|
$out = sanitize_staff_html('<img src="data:image/svg+xml;base64,AAAA">');
|
|
$check(!str_contains($out, 'svg'), 'svg-Datenbild muss ersetzt werden: ' . $out);
|
|
|
|
// Normales Rich-Text-HTML bleibt erhalten
|
|
$ok = '<p>Hallo <b>Welt</b>, <i>kursiv</i> und <a href="https://example.org/a?x=1&y=2">ein Link</a>.</p><ul><li>Eins</li><li>Zwei</li></ul>'
|
|
. '<table border="1"><tr><td colspan="2" style="color: red">Zelle</td></tr></table><blockquote>Zitat</blockquote><br>';
|
|
$out = sanitize_staff_html($ok);
|
|
foreach (['<p>', '<b>Welt</b>', '<i>kursiv</i>', 'href="https://example.org/a?x=1&y=2"', '<li>Zwei</li>', 'colspan="2"', 'style="color: red"', '<blockquote>Zitat</blockquote>', '<br>'] as $needle) {
|
|
$check(str_contains($out, $needle), "Erlaubtes HTML ging verloren ({$needle}): {$out}");
|
|
}
|
|
$check(sanitize_staff_html(' ') === '', 'Leerer Text');
|
|
$check(sanitize_staff_html('Preis < 5 und > 3') === 'Preis < 5 und > 3', 'Kleiner/Größer im Fließtext: ' . sanitize_staff_html('Preis < 5 und > 3'));
|
|
|
|
if ($failures) {
|
|
fwrite(STDERR, "FEHLER:\n- " . implode("\n- ", $failures) . "\n");
|
|
exit(1);
|
|
}
|
|
echo 'Sanitize HTML checks passed (' . count($payloads) . " Angriffe).\n";
|