]*)?)>/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' => 'Hallo', 'script verschachtelt' => 'ipt>alert(1)ipt>', 'img onerror' => '', 'img onerror quoted' => '', 'unterminiert' => 'Text ' '<', 'a javascript' => 'x', 'a javascript entity' => 'x', 'a javascript tab' => 'x', 'a data' => 'x', 'a onclick' => 'x', 'a attribut ausbruch' => 'x', 'img svg data' => '', 'style expression' => 'x', 'style url' => 'x', 'style url entity' => 'x', 'style position' => 'Bitte Passwort eingeben', 'style ausbruch' => 'x', 'iframe' => '', 'iframe srcdoc' => '', 'form' => 'Senden', 'meta refresh' => '', 'base' => '', 'object' => '', 'math' => 'x', 'kommentar' => '-->', 'bedingt' => '', 'cdata' => 'alert(1)]]>', 'attribut ohne quotes' => '', 'grossschreibung' => '', 'null byte' => "alert(1)", 'tag mit newline' => "", 'slash statt space' => '', 'details ontoggle' => 'x', 'td background' => 'x', ]; 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 x'); $check(!str_contains($out, 'javascript') && str_contains($out, 'rel="noopener noreferrer"'), 'javascript-Link: ' . $out); $out = sanitize_staff_html('x'); $check(!str_contains($out, 'position') && str_contains($out, 'color: red'), 'position muss entfernt, color bleiben: ' . $out); $out = sanitize_staff_html('x'); $check(!str_contains($out, 'url') && !str_contains($out, 'evil'), 'url() im Stil: ' . $out); $out = sanitize_staff_html(''); $check(!str_contains($out, 'tracker.example'), 'http-Bild muss ersetzt werden: ' . $out); $out = sanitize_staff_html(''); $check(!str_contains($out, 'svg'), 'svg-Datenbild muss ersetzt werden: ' . $out); // Normales Rich-Text-HTML bleibt erhalten $ok = 'Hallo Welt, kursiv und ein Link.EinsZwei' . 'ZelleZitat'; $out = sanitize_staff_html($ok); foreach (['', 'Welt', 'kursiv', 'href="https://example.org/a?x=1&y=2"', 'Zwei', 'colspan="2"', 'style="color: red"', 'Zitat', ''] as $needle) { $check(str_contains($out, $needle), "Erlaubtes HTML ging verloren ({$needle}): {$out}"); } // Skalierte Bilder (Signatur): Breite als Attribut und Stil bleiben erhalten, riskante Stile nicht $out = sanitize_staff_html(''); $check(str_contains($out, 'src="https://example.org/uploads/logo.png"') && str_contains($out, 'width="130"') && str_contains($out, 'width: 130px') && str_contains($out, 'height: auto'), 'skaliertes Bild behält Adresse, Breite und Stil: ' . $out); $out = sanitize_staff_html(''); $check(str_contains($out, 'width: 130px') && !str_contains($out, 'position') && !str_contains($out, 'evil'), 'beim skalierten Bild werden riskante Stile trotzdem entfernt: ' . $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";
Hallo Welt, kursiv und ein Link.
Zitat
', 'Welt', 'kursiv', 'href="https://example.org/a?x=1&y=2"', '