Upload via GUI (258 Dateien)
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
<?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";
|
||||
Reference in New Issue
Block a user