Replace fragile HTML allowlist sanitizer

This commit is contained in:
Troy Grunt
2026-02-15 15:01:50 +01:00
parent cb115bac40
commit 644e03da9b
2 changed files with 93 additions and 141 deletions

View File

@@ -51,19 +51,20 @@
- Fehler enthalten Query-Kontext ohne Secrets.
- Verhalten entspricht der definierten Error-Strategie.
- #TODO Replace fragile HTML allowlist sanitizer
- Aufwand: `M`
- Labels: `security`, `string`
- Akzeptanzkriterien:
- `onlySimpleHTML()` wird durch robusteren Ansatz ersetzt.
- Erlaubte Tags sind konfigurierbar dokumentiert.
- Regression-Tests fuer typische Eingaben vorhanden.
- #TODO Code-Qualitaet
- Sammel-Issue: Naming-Konvention, SQL-Binding-Refactor, Legacy-Markierung, Markdown-Konsolidierung, klare Modulgrenzen.
- Code-Qualitaet (aufgeteilt in Unter-Issues)
- Aufwand: `L`
- Empfehlung: in 3-5 Unter-Issues aufteilen.
- Labels: `quality`, `refactor`
- Unter-Issues:
- Define and enforce naming conventions for functions, files and constants.
- Refactor SQL binding helpers to one consistent, typed API surface.
- Mark legacy functions/modules (`@deprecated`) and document replacement path.
- Consolidate Markdown docs (README + API notes) into one canonical structure.
- Clarify module boundaries and ownership (I/O, SQL, parsing, formatting).
- Akzeptanzkriterien:
- Kurzer Styleguide in `README.md` vorhanden und auf bestehende Dateien angewendet.
- Keine neuen Legacy-Einstiege ohne Markierung und Migrationshinweis.
- SQL-Helper nutzen einheitliche Signaturen in geaenderten Modulen.
- Modulgrenzen sind in Doku und Dateistruktur konsistent nachvollziehbar.
- #TODO Tests und Tooling

View File

@@ -129,137 +129,88 @@ function markUp(string $text): string {
}
return $r;
}
function onlySimpleHTML(string $s): string {
$s = str_replace ( array (
'<',
'>'
), array (
'{{|-&lt;-|}}',
'{{|-&gt;-|}}'
), $s );
$s = str_replace ( array (
'{{|-&lt;-|}}b{{|-&gt;-|}}',
'{{|-&lt;-|}}b/{{|-&gt;-|}}'
), array (
'<b>',
'<b/>'
), $s );
$s = str_replace ( array (
'{{|-&lt;-|}}u{{|-&gt;-|}}',
'{{|-&lt;-|}}u/{{|-&gt;-|}}'
), array (
'<u>',
'<u/>'
), $s );
$s = str_replace ( array (
'{{|-&lt;-|}}i{{|-&gt;-|}}',
'{{|-&lt;-|}}i/{{|-&gt;-|}}'
), array (
'<i>',
'<i/>'
), $s );
$s = str_replace ( array (
'{{|-&lt;-|}}span{{|-&gt;-|}}',
'{{|-&lt;-|}}span/{{|-&gt;-|}}'
), array (
'<span>',
'<span/>'
), $s );
$s = str_replace ( array (
'{{|-&lt;-|}}b{{|-&gt;-|}}',
'{{|-&lt;-|}}b/{{|-&gt;-|}}'
), array (
'<b>',
'<b/>'
), $s );
$s = str_replace ( array (
'{{|-&lt;-|}}br{{|-&gt;-|}}',
'{{|-&lt;-|}}br/{{|-&gt;-|}}'
), array (
'<br>',
'<br/>'
), $s );
$s = str_replace ( array (
'{{|-&lt;-|}}h1{{|-&gt;-|}}',
'{{|-&lt;-|}}h1/{{|-&gt;-|}}'
), array (
'<h1>',
'<h1/>'
), $s );
$s = str_replace ( array (
'{{|-&lt;-|}}h2{{|-&gt;-|}}',
'{{|-&lt;-|}}h2/{{|-&gt;-|}}'
), array (
'<h2>',
'<h2/>'
), $s );
$s = str_replace ( array (
'{{|-&lt;-|}}h3{{|-&gt;-|}}',
'{{|-&lt;-|}}h3/{{|-&gt;-|}}'
), array (
'<h3>',
'<h3/>'
), $s );
$s = str_replace ( array (
'{{|-&lt;-|}}h4{{|-&gt;-|}}',
'{{|-&lt;-|}}h4/{{|-&gt;-|}}'
), array (
'<h4>',
'<h4/>'
), $s );
$s = str_replace ( array (
'{{|-&lt;-|}}h5{{|-&gt;-|}}',
'{{|-&lt;-|}}h5/{{|-&gt;-|}}'
), array (
'<h5>',
'<h5/>'
), $s );
$s = str_replace ( array (
'{{|-&lt;-|}}h6{{|-&gt;-|}}',
'{{|-&lt;-|}}h6/{{|-&gt;-|}}'
), array (
'<h6>',
'<h6/>'
), $s );
$s = str_replace ( array (
'{{|-&lt;-|}}li{{|-&gt;-|}}',
'{{|-&lt;-|}}li/{{|-&gt;-|}}'
), array (
'<li>',
'<li/>'
), $s );
$s = str_replace ( array (
'{{|-&lt;-|}}ul{{|-&gt;-|}}',
'{{|-&lt;-|}}ul/{{|-&gt;-|}}'
), array (
'<ul>',
'<ul/>'
), $s );
$s = str_replace ( array (
'{{|-&lt;-|}}ol{{|-&gt;-|}}',
'{{|-&lt;-|}}ol/{{|-&gt;-|}}'
), array (
'<ol>',
'<ol/>'
), $s );
$s = str_replace ( array (
'{{|-&lt;-|}}pre{{|-&gt;-|}}',
'{{|-&lt;-|}}pre/{{|-&gt;-|}}'
), array (
'<pre>',
'<pre/>'
), $s );
function onlySimpleHTML(string $s, ?array $allowedTags = null): string {
if ($s === '') {
return '';
}
// cleanup
$s = str_replace ( array (
'{{|-',
'-|}}'
), array (
'',
''
), $s );
if ($allowedTags === null) {
$allowedTags = array (
'b',
'u',
'i',
'span',
'br',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'li',
'ul',
'ol',
'pre'
);
}
return $s;
$allow = array_fill_keys ( array_map ( 'strtolower', $allowedTags ), true );
$selfClosing = array (
'br' => true
);
$parts = preg_split ( '/(<[^>]*>)/', $s, - 1, PREG_SPLIT_DELIM_CAPTURE );
if ($parts === false) {
return htmlspecialchars ( $s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
}
$out = '';
foreach ( $parts as $part ) {
if ($part === '') {
continue;
}
if ($part[0] !== '<') {
$out .= $part;
continue;
}
if (preg_match ( '/^<\s*(\/?)\s*([a-z0-9]+)\s*(\/?)\s*>$/i', $part, $m ) !== 1) {
$out .= htmlspecialchars ( $part, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
continue;
}
$isClose = ($m[1] === '/');
$tag = strtolower ( $m[2] );
$isSelfClose = ($m[3] === '/');
if (! isset ( $allow[$tag] )) {
$out .= htmlspecialchars ( $part, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
continue;
}
if ($isClose) {
if (isset ( $selfClosing[$tag] )) {
continue;
}
$out .= "</{$tag}>";
continue;
}
if ($isSelfClose && ! isset ( $selfClosing[$tag] )) {
$out .= "</{$tag}>";
continue;
}
if (isset ( $selfClosing[$tag] )) {
$out .= "<{$tag}>";
continue;
}
$out .= "<{$tag}>";
}
return $out;
}
function linkify(string $input): string {
$pattern = '@(http(s)?://[a-zA-Z0-9/\.\#\-\_]*)@';