From 644e03da9b150312e5547404fca82b9bb688e92f Mon Sep 17 00:00:00 2001 From: Troy Grunt Date: Sun, 15 Feb 2026 15:01:50 +0100 Subject: [PATCH] Replace fragile HTML allowlist sanitizer --- NEXT_STEPS.md | 25 +++--- string.php | 209 +++++++++++++++++++------------------------------- 2 files changed, 93 insertions(+), 141 deletions(-) diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index 3b884b3..8b1fbeb 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -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 diff --git a/string.php b/string.php index 8af8939..fc62864 100644 --- a/string.php +++ b/string.php @@ -129,137 +129,88 @@ function markUp(string $text): string { } return $r; } -function onlySimpleHTML(string $s): string { - $s = str_replace ( array ( - '<', - '>' - ), array ( - '{{|-<-|}}', - '{{|->-|}}' - ), $s ); - $s = str_replace ( array ( - '{{|-<-|}}b{{|->-|}}', - '{{|-<-|}}b/{{|->-|}}' - ), array ( - '', - '' - ), $s ); - $s = str_replace ( array ( - '{{|-<-|}}u{{|->-|}}', - '{{|-<-|}}u/{{|->-|}}' - ), array ( - '', - '' - ), $s ); - $s = str_replace ( array ( - '{{|-<-|}}i{{|->-|}}', - '{{|-<-|}}i/{{|->-|}}' - ), array ( - '', - '' - ), $s ); - $s = str_replace ( array ( - '{{|-<-|}}span{{|->-|}}', - '{{|-<-|}}span/{{|->-|}}' - ), array ( - '', - '' - ), $s ); - $s = str_replace ( array ( - '{{|-<-|}}b{{|->-|}}', - '{{|-<-|}}b/{{|->-|}}' - ), array ( - '', - '' - ), $s ); - $s = str_replace ( array ( - '{{|-<-|}}br{{|->-|}}', - '{{|-<-|}}br/{{|->-|}}' - ), array ( - '
', - '
' - ), $s ); - $s = str_replace ( array ( - '{{|-<-|}}h1{{|->-|}}', - '{{|-<-|}}h1/{{|->-|}}' - ), array ( - '

', - '

' - ), $s ); - $s = str_replace ( array ( - '{{|-<-|}}h2{{|->-|}}', - '{{|-<-|}}h2/{{|->-|}}' - ), array ( - '

', - '

' - ), $s ); - $s = str_replace ( array ( - '{{|-<-|}}h3{{|->-|}}', - '{{|-<-|}}h3/{{|->-|}}' - ), array ( - '

', - '

' - ), $s ); - $s = str_replace ( array ( - '{{|-<-|}}h4{{|->-|}}', - '{{|-<-|}}h4/{{|->-|}}' - ), array ( - '

', - '

' - ), $s ); - $s = str_replace ( array ( - '{{|-<-|}}h5{{|->-|}}', - '{{|-<-|}}h5/{{|->-|}}' - ), array ( - '

', - '
' - ), $s ); - $s = str_replace ( array ( - '{{|-<-|}}h6{{|->-|}}', - '{{|-<-|}}h6/{{|->-|}}' - ), array ( - '
', - '
' - ), $s ); - $s = str_replace ( array ( - '{{|-<-|}}li{{|->-|}}', - '{{|-<-|}}li/{{|->-|}}' - ), array ( - '
  • ', - '
  • ' - ), $s ); - $s = str_replace ( array ( - '{{|-<-|}}ul{{|->-|}}', - '{{|-<-|}}ul/{{|->-|}}' - ), array ( - '
      ', - '
        ' - ), $s ); - $s = str_replace ( array ( - '{{|-<-|}}ol{{|->-|}}', - '{{|-<-|}}ol/{{|->-|}}' - ), array ( - '
          ', - '
            ' - ), $s ); - $s = str_replace ( array ( - '{{|-<-|}}pre{{|->-|}}', - '{{|-<-|}}pre/{{|->-|}}' - ), array ( - '
            ',
            -          '
            '
            -  ), $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 .= "";
            +      continue;
            +    }
            +
            +    if ($isSelfClose && ! isset ( $selfClosing[$tag] )) {
            +      $out .= "";
            +      continue;
            +    }
            +
            +    if (isset ( $selfClosing[$tag] )) {
            +      $out .= "<{$tag}>";
            +      continue;
            +    }
            +
            +    $out .= "<{$tag}>";
            +  }
            +
            +  return $out;
             }
             function linkify(string $input): string {
               $pattern = '@(http(s)?://[a-zA-Z0-9/\.\#\-\_]*)@';