Thomas Bandt

Über mich | Kontakt | Archiv

HTML zuverlässig mit dem Html Agility Pack parsen

Ein immer wiederkehrendes Szenario bei Webapplikationen ist naturgemäß das Posten von Text(-Nachrichten) über Formulare und das Ausgeben dieser Texte, sei es in Foren, auf Profilseiten oder was auch immer. Da der Mensch bequem ist, setzt man ihm für seine Formatierungen einen WYSIWYG-Editor vor die Nase (z.B. CKEditor, sehr zu empfehlen), über den er ganz bequem seine Formatierungen für seinen Text vornehmen kann.

Sofern das alles im Kontext einer geschlussenen Umgebung passiert, also z.B. im Backend eines Content Management Systems, ist das auch völlig unproblematisch. Kritisch wird es immer dort, wo nicht ganz klar ist, wer denn den Text verfasst und vor allem wenn dieser öffentlich, also für Dritte sichtbar, ausgegeben wird. Warum ist das kritisch? Weil dies ein riesiges Einfallstor für so genanntes Cross-Site Scripting ist.

Besonders sensibilisiert bin ich dafür, seit mich vor zwei Jahren ein Kunde bat, mal seine neue Website unter die Lupe zu nehmen. Ich brauchte gar nicht viel meiner ohnehin nicht vorhandenen kriminellen Energie oder besondere Erfahrung, mir reichte dieses Cheat Sheet um die (pakistanischen) Entwickler der Website zur Verzweiflung zu treiben.

Im Web gilt seit jeher: All Input Is Evil.

In meinem aktuellen Projekt war ich ein wenig am Überlegen, wie weit ich das Ganze wirklich absichern muss - denn die geschriebenen Texte werden nicht öffentlich im Web sondern nur nach vorherigem Login einem bekannten Nutzerkreis bereitgestellt. Aber dennoch - es sollen ja schon Pferder vor Apotheken gekotzt haben ...

Mein erster Gedanke galt BBCode, für welches auch ein Plugin für den CKEditor gibt, das sich leicht erweitern lässt. Aber wenn man mal ein wenig rumsucht, dann findet man auch hier recht große Angriffsflächen - da lohnt in meinen Augen der ganze Aufwand nicht.

Meine Entscheidung fiel also auf die folgende Kombination:

  1. CKEditor mit sehr limitierten Formatierungsoptionen und ohne Einbindung von Bildern.
  2. Parsing aller Tags und Attribute (!) und Test gegen eine Whitelist.

Damit das funktioniert braucht es einen richtigen HTML-Parser, der den übergebenen Text in die einzelnen Objekte zerlegt - mit Regular Expressions läuft man hier gegen eine Wand. Ich habe mich für das Html Agility Pack entschieden, was recht ausgereift ist und ein passables API bietet.

Der Code ist recht einfach:

   1:  [HttpPost]
   2:  public ActionResult Index(HtmlModel model)
   3:  {
   4:      model.Html = ParseHtml(model.Html);
   5:      return View(model);
   6:  }
   7:   
   8:  private static string ParseHtml(string html)
   9:  {
  10:   
  11:      var doc = new HtmlDocument();
  12:      doc.OptionAutoCloseOnEnd = true;
  13:      doc.LoadHtml(html);
  14:              
  15:      var nodeWhiteList = new List<string> { "#text", "#document", "a", "br", "ul", "li" };
  16:      var attributeWhiteList = new List<string> { "href" };
  17:              
  18:      RemoveElementsNotInWhiteLists(doc.DocumentNode, nodeWhiteList, attributeWhiteList);
  19:   
  20:      return doc.DocumentNode.WriteTo().Replace("<br>", "<br />");
  21:   
  22:  }
  23:   
  24:  private static void RemoveElementsNotInWhiteLists(HtmlNode node, 
  25:      IEnumerable<string> nodeWhiteList, IEnumerable<string> attributeWhiteList)
  26:  {
  27:   
  28:      if (!nodeWhiteList.Contains(node.Name) || node.Attributes.Any(a => a.Value.ToLower().Contains("javascript")))
  29:      {
  30:          node.Remove();
  31:          return;
  32:      }
  33:   
  34:      node.Attributes
  35:              .Where(attribute => !attributeWhiteList.Contains(attribute.Name))
  36:              .ToList()
  37:              .ForEach(attribute => attribute.Remove());
  38:              
  39:      node.ChildNodes
  40:              .ToList()
  41:              .ForEach(childNode => RemoveElementsNotInWhiteLists(childNode, nodeWhiteList, attributeWhiteList));
  42:   
  43:  }

Wie man sieht: die meiste Arbeit wird bereits von der Komponente erledigt und gekapselt, so dass man sich auf die eigentliche Aufgabe konzentrieren kann.

Zunächst werden die erlaubten Tags und Attribute definiert und anschließend für alle Nodes die Methode RemoveElementsNotInWhiteLists() aufgerufen, in welcher dann die unerlaubten Attribute bzw. Tags entfernt werden.

Zwei Ausnahmen sind noch drin: ein javascript-Aufruf in einem Attribut (vornehmlich in href-Attributen) kann in meinem Fall nicht vorkommen, falls es also dennoch reingeschrieben wurde, dann wird gleich das komplette Tag entfernt. Und leider werden bei der Ausgabe Tags wie <img /> und <br /> nicht richtig geschlossen, sofern man kein komplettes HTML-Dokument verarbeitet. Daher am Ende pragmatisch noch die Ersetzung von <br> zu <br /> ...

Das Ergebnis kann sich sehen lassen:

Den Code habe ich als ausführbares MVC3-Projekt angehangen.

Downloads



« Zurück  |  Weiter »