Validierung von HTML-Benutzereingaben

Cross-Site-Scripting sichere Prüfung von HTML-Eingaben

Wer seinen Nutzern die Verwendung von HTML-Elementen innerhalb von Eingaben wie zum Beispiel Kommentaren gestatten will, der muss sich Gedanken darüber machen, wie er gewährleistet, dass dabei die Möglichkeit des Cross-Site-Scriptings ausgeschlossen wird.

Das wohl einfachste Vorgehen dabei ist es natürlich, HTML-Eingaben gar nicht oder nur sehr eingeschränkt zuzulassen. Indem man quasi eine Whitelist mit Elementen, die der Nutzer verwenden darf, erstellt und generell alle Attribute verbietet, lässt sich Cross-Site-Scripting auf eine sehr einfache Weise verhindern. In Listing 1 sehen Sie beispielsweise die PHP-Implementierung eines solchen Vorgehens.

Listing 1: HTML-Elemente aus einer Whitelist zulassen (ohne Attribute)

function parse_whitelist_html($string, $whitelist)
{
  $string = str_replace('<', '&lt;', str_replace('>', '&gt;', $string));
  if($whitelist != null)
    foreach($whitelist as $element)
      $string = str_replace('&lt;' . $element . '&gt;', '<' . $element . '>', str_replace('&lt;/' . $element . '&gt;', '</' . $element . '>', $string));
  return $string;
}

Der Nachteil dieses Verfahrens jedoch dürfte offensichtlich sein: HTML-Formatierungen können lediglich als Formatierungsvorgaben verwendet werden, also beispielsweise dazu, um Überschriften auszuzeichnen oder Textfragmente fett zu markieren, Elemente wie Links können so jedoch nicht realisiert werden, ebenso wenig wie Bilder oder CSS-Formatierungen.

Wenn Sie Unterstützung für derartige Elemente zulassen wollen, können Sie das ebenfalls wieder mit einer Whitelist realisieren, indem Sie jedem erlaubtem HTML-Element eine Liste erlaubter Attribute zuweisen. Das ist notwendig, um die Einbettung von JavaScript-Code über sogenannte Event-Handler, also beispielsweise über das onclick-Attribut oder ähnliche Attribute zu verhindern.

Doch insbesondere dann, wenn Sie die Verwendung des Anker-HTML-Elements gestatten möchten, kommen dabei weitere Probleme auf Sie zu. So ist es möglich, innerhalb des href-Attributs JavaScript-Anweisungen zu notieren, die dann bei einem Klick auf den Link ausgeführt werden. Beispielsweise würde ein Link der Form <a href=“javascript:alert('XSS')“>Link</a> einen Popup-Dialog mit dem Text „XSS“ öffnen. Das möchten Sie in aller Regel verhindern, deshalb ist es notwendig, auch den Wert eines Attributs zu validieren. Das können Sie nun mithilfe regulärer Ausdrücke tun.[1]

Eine Implementierung eines solchen Whitelist-Verfahrens in PHP sehen Sie in Listing 2.

Listing 2: Eine Whitelist mit vorgegebenen Attributen und Regeln zur Validierung dieser Werte.

function parse_whitelist_html($string, $whitelist)
{
  $string = str_replace('lt;', '&lt;', str_replace('>', '&gt;', $string));
  if($whitelist != null)
  {
    foreach($whitelist as $element => $attr_list)
    {
      $substrings = array();
      $tag_start = '&lt;' . $element;
      $i = (-1) * strlen($tag_start);
      while($i = strpos($string, $tag_start, $i + strlen($tag_start)))
        $substrings[] = str_split(substr($string, $i + strlen($tag_start)));
      $elements = array(); //Array mit allen gefundenen Elementen.
      foreach($substrings as $substring)
      {
        $num_single_quotes = 0;
        $num_double_quotes = 0;
        for($i = 0; $i < (count($substring) - 3); $i++)
        {
          if($substring[$i] == '"' && ($num_single_quotes % 2) != 1)
            $num_double_quotes++;
          else if($substring[$i] == '\'' && ($num_double_quotes % 2) != 1)
            $num_single_quotes++;
          else if(($substring[$i] == '&') && ($substring[$i + 1] == 'g') && ($substring[$i + 2] == 't') && ($substring[$i + 3] == ';') && ((($num_single_quotes + $num_double_quotes) % 2) == 0))
          {
            $elements[] = implode(array_slice($substring, 0, $i));
            break;
          }
        }
      }
      $new_elements = array();
      for($i = 0; $i < count($elements); $i++)
      {
        $work_str = $elements[$i];
        foreach($attr_list as $attr_name => $attr_regex)
        {
          if($start = strpos($work_str, $attr_name . '='))
          {
            $start += strlen($attr_name . '=');
            $quote_char = substr($work_str, $start, 1);
            $end = strpos($work_str, $quote_char, ++$start);
            if(preg_match($attr_regex, substr($work_str, $start, $end - $start)) == 1)
              $new_elements[$i][$attr_name] = substr($work_str, $start, $end - $start);
          }
        }
        $elements[$i] = '&lt;' . $element . $elements[$i] . '&gt;';
      }
      $replace_elements = array();
      for($i = 0; $i < count($new_elements); $i++)
      {
        $replace_elements[$i] = '<' . $element;
        foreach($new_elements[$i] as $attribute => $value)
        $replace_elements[$i] .= ' ' . $attribute . '="' . $value . '"';
        $replace_elements[$i] .= '>';
      }
      if(count($replace_elements) > 0)
        for($i = 0; $i < count($replace_elements); $i++)
          $string = str_replace($elements[$i], $replace_elements[$i], $string);
      else
        $string = str_replace('&lt;' . $element . '&gt;', '<' . $element . '>', $string);
      $string = str_replace('&lt;/' . $element . '&gt;', '</' . $element . '>', $string);
    }
  }
  return $string;
}

Im Download-Bereich finden Sie eine kleine PHP-Bibliothek zur Validierung von HTML-Eingaben nach den hier beschriebenen Verfahrensweisen.

[1] Theoretisch könnten Sie auch eine ganze Eingabezeichenkette mit einem regulären Ausdruck valideren, dabei gibt es jedoch zwei Probleme: Erstens ist ein solcher Regulärer Ausdruck unglaublich unübersichtlich und daher äußerst fehleranfällig, zweitens lässt sich damit nur eine Valide-Eingabe feststellen. Enthält eine Eingabe ungültige Elemente, müsste sie vollständig verworfen werden. Ein solches Verhalten ist in der Regel unerwünscht.