Bereinigen / Umschreiben von HTML auf der Clientseite

81

Ich muss externe Ressourcen anzeigen, die über domänenübergreifende Anforderungen geladen wurden, und sicherstellen, dass nur " sichere " Inhalte angezeigt werden.

Könnte Prototypen verwenden String # stripScripts verwenden , um Skriptblöcke zu entfernen. Aber Handler wie onclickoder onerrorsind immer noch da.

Gibt es eine Bibliothek, die das zumindest kann?

  • Strip-Skriptblöcke,
  • töte DOM-Handler,
  • Entfernen Sie Tags auf der schwarzen Liste (z. B.: embedoderobject ).

Gibt es also Links und Beispiele zu JavaScript?

Aemkei
quelle
12
Traue keinen Antworten, die dies durch reguläre Ausdrücke tun könnten stackoverflow.com/questions/1732348/…
Mikko Ohtamaa
Wie ist das sicher? Können Benutzer das Javascript einer Seite nicht bearbeiten?
Daniel sagt Reinstate Monica
Ja, es ist nicht "sicher", es sei denn, Sie versuchen lediglich, Fehler durch vertrauenswürdige Benutzer zu verhindern.
Scott

Antworten:

111

Update 2016: Es gibt jetzt ein Google Closure- Paket, das auf dem Caja-Desinfektionsmittel basiert.

Es verfügt über eine übersichtlichere API, wurde neu geschrieben, um APIs zu berücksichtigen, die in modernen Browsern verfügbar sind, und interagiert besser mit Closure Compiler.


Schamloser Plug: Unter caja / plugin / html-sanitizer.js finden Sie einen clientseitigen HTML-Sanitizer, der gründlich überprüft wurde.

Es ist auf der weißen Liste, nicht auf der schwarzen Liste, aber die Whitelists können gemäß CajaWhitelists konfiguriert werden


Wenn Sie alle Tags entfernen möchten, gehen Sie wie folgt vor:

var tagBody = '(?:[^"\'>]|"[^"]*"|\'[^\']*\')*';

var tagOrComment = new RegExp(
    '<(?:'
    // Comment body.
    + '!--(?:(?:-*[^->])*--+|-?)'
    // Special "raw text" elements whose content should be elided.
    + '|script\\b' + tagBody + '>[\\s\\S]*?</script\\s*'
    + '|style\\b' + tagBody + '>[\\s\\S]*?</style\\s*'
    // Regular name
    + '|/?[a-z]'
    + tagBody
    + ')>',
    'gi');
function removeTags(html) {
  var oldHtml;
  do {
    oldHtml = html;
    html = html.replace(tagOrComment, '');
  } while (html !== oldHtml);
  return html.replace(/</g, '&lt;');
}

Die Leute werden Ihnen sagen, dass Sie ein Element erstellen innerHTMLund das innerTextoder zuweisen und dann abrufen textContentund dann Entitäten darin entkommen können. TU das nicht. Es ist anfällig für XSS-Injection, da <img src=bogus onerror=alert(1337)>der onerrorHandler auch dann ausgeführt wird, wenn der Knoten niemals mit dem DOM verbunden ist.

Mike Samuel
quelle
5
Großartig, es sieht so aus, als gäbe es hier eine kleine Dokumentation: code.google.com/p/google-caja/wiki/JsHtmlSanitizer
tmcw
3
Der Caja HTML-Desinfektionscode sieht gut aus, erfordert jedoch etwas Klebercode (benachbart cssparser.js, aber was noch wichtiger ist, das html4Objekt). Darüber hinaus verschmutzt es die globale windowEigenschaft. Gibt es eine For-the-Web-Version dieses Codes? Wenn nicht, sehen Sie einen besseren Weg, um eines zu erstellen und zu warten, als ein separates Projekt dafür zu erstellen?
Phihag
1
@phihag, Fragen Sie bei google-caja-diskutieren und sie könnten Sie auf eine verpackte verweisen. Ich glaube, dass die Verschmutzung von Fensterobjekten der Abwärtskompatibilität dient, und daher benötigt jede neue Paketversion dies möglicherweise nicht.
Mike Samuel
1
Es stellt sich heraus, dass es bereits ein Paket für Webbrowser gibt.
Phihag
2
@phihag Dieses Paket ist für NodeJS, nicht für Browser.
Jeffery bis
40

Das HTML-Desinfektionsmittel von Google Caja kann "webfähig" gemacht werden, indem es in einen Web-Worker eingebettet wird . Alle vom Desinfektionsprogramm eingeführten globalen Variablen sind im Worker enthalten, und die Verarbeitung erfolgt in einem eigenen Thread.

Für Browser, die Web Worker nicht unterstützen, können wir einen Iframe als separate Umgebung für das Desinfektionsprogramm verwenden. Timothy Chien verfügt über eine Polyfüllung , die genau dies tut und Iframes zur Simulation von Web Workern verwendet, sodass dieser Teil für uns erledigt wird.

Das Caja-Projekt verfügt über eine Wiki-Seite zur Verwendung von Caja als eigenständiges clientseitiges Desinfektionsmittel :

  • Überprüfen Sie die Quelle und erstellen Sie sie durch Ausführen ant
  • Fügen Sie html-sanitizer-minified.jsoder html-css-sanitizer-minified.jsin Ihre Seite ein
  • Anruf html_sanitize(...)

Das Arbeiterskript muss nur die folgenden Anweisungen befolgen:

importScripts('html-css-sanitizer-minified.js'); // or 'html-sanitizer-minified.js'

var urlTransformer, nameIdClassTransformer;

// customize if you need to filter URLs and/or ids/names/classes
urlTransformer = nameIdClassTransformer = function(s) { return s; };

// when we receive some HTML
self.onmessage = function(event) {
    // sanitize, then send the result back
    postMessage(html_sanitize(event.data, urlTransformer, nameIdClassTransformer));
};

(Es wird etwas mehr Code benötigt, um die Simworker-Bibliothek zum Laufen zu bringen, aber für diese Diskussion ist dies nicht wichtig.)

Demo: https://dl.dropbox.com/u/291406/html-sanitize/demo.html

Jeffery To
quelle
Gute Antwort. Jeffrey, können Sie erklären, warum die Desinfektion überhaupt von einem Web-Mitarbeiter durchgeführt werden muss?
Austin Wang
@AustinWang Web-Mitarbeiter sind nicht unbedingt erforderlich. Da die Bereinigung jedoch möglicherweise rechenintensiv sein kann und keine Benutzerinteraktion erfordert, ist sie für die Aufgabe gut geeignet. (Ich erwähnte auch, dass globale Variablen in der Hauptantwort enthalten sind.)
Jeffery bis
Ich kann keine anständige Dokumentation für diese Bibliothek finden. Wo / wie gebe ich meine Whitelist mit Elementen und Attributen an?
AsGoodAsItGets
@AsGoodAsItGets Wie in der aktuellen Version durch einen Kommentar beschrieben , nameIdClassTransformerwird für jeden HTML-Namen, jede Element-ID und jede Liste von Klassen aufgerufen. Beim Zurückgeben nullwird das Attribut gelöscht. Durch Bearbeiten der JSON-Dateien in src / com / google / caja / lang / html können Sie auch anpassen, welche Elemente und Attribute in die Whitelist aufgenommen werden.
Jeffery bis
@ JefferTo Es tut mir leid, vielleicht bin ich zu dumm, aber ich verstehe es nicht. Die JSON-Dateien, auf die Sie verweisen, werden in Ihrem obigen Beispiel und Ihrer Demo nicht verwendet. Ich möchte die Bibliothek in einem Browser verwenden, also habe ich mir Ihre Demo angesehen. Können Sie die nameIdClassTranformerobige Funktion ändern, z. B. um alle <script>Tags abzulehnen und zu akzeptieren <b>und <i>Tags?
AsGoodAsItGets
20

Vertraue niemals dem Kunden. Wenn Sie eine Serveranwendung schreiben, gehen Sie davon aus, dass der Client immer unhygienische, böswillige Daten übermittelt. Es ist eine Faustregel, die Sie vor Ärger bewahrt. Wenn Sie können, würde ich empfehlen, alle Validierungen und Hygienemaßnahmen im Servercode durchzuführen, von denen Sie wissen, dass sie (in angemessenem Maße) nicht zu kurz kommen. Vielleicht könnten Sie eine serverseitige Webanwendung als Proxy für Ihren clientseitigen Code verwenden, der vom Drittanbieter abgerufen und bereinigt wird, bevor er an den Client selbst gesendet wird?

[Bearbeiten] Es tut mir leid, ich habe die Frage falsch verstanden. Ich stehe jedoch zu meinem Rat. Ihre Benutzer sind wahrscheinlich sicherer, wenn Sie den Server bereinigen, bevor Sie ihn an sie senden.

Sean Edwards
quelle
19
Mit zunehmender Popularität von node.js kann eine Javascript-Lösung auch eine serverseitige Lösung sein. So bin ich zumindest hier gelandet. Trotzdem ist dies ein ausgezeichneter Rat, nach dem man leben kann.
Nicholas Flynt
15

Jetzt, da alle gängigen Browser Sandbox-Iframes unterstützen, gibt es einen viel einfacheren Weg, den ich für sicher halte . Ich würde es lieben, wenn diese Antwort von Personen überprüft werden könnte, die mit dieser Art von Sicherheitsproblemen besser vertraut sind.

HINWEIS: Diese Methode funktioniert in IE 9 und früheren Versionen definitiv nicht. In dieser Tabelle finden Sie Browserversionen, die Sandboxing unterstützen. (Hinweis: Die Tabelle scheint zu sagen, dass es in Opera Mini nicht funktioniert, aber ich habe es gerade ausprobiert und es hat funktioniert.)

Die Idee ist, einen versteckten Iframe mit deaktiviertem JavaScript zu erstellen, Ihren nicht vertrauenswürdigen HTML-Code einzufügen und ihn analysieren zu lassen. Anschließend können Sie den DOM-Baum durchsuchen und die Tags und Attribute kopieren, die als sicher gelten.

Die hier gezeigten Whitelists sind nur Beispiele. Was am besten zur Whitelist gehört, hängt von der Anwendung ab. Wenn Sie eine komplexere Richtlinie benötigen als nur Whitelists mit Tags und Attributen, kann diese Methode berücksichtigt werden, nicht jedoch dieser Beispielcode.

var tagWhitelist_ = {
  'A': true,
  'B': true,
  'BODY': true,
  'BR': true,
  'DIV': true,
  'EM': true,
  'HR': true,
  'I': true,
  'IMG': true,
  'P': true,
  'SPAN': true,
  'STRONG': true
};

var attributeWhitelist_ = {
  'href': true,
  'src': true
};

function sanitizeHtml(input) {
  var iframe = document.createElement('iframe');
  if (iframe['sandbox'] === undefined) {
    alert('Your browser does not support sandboxed iframes. Please upgrade to a modern browser.');
    return '';
  }
  iframe['sandbox'] = 'allow-same-origin';
  iframe.style.display = 'none';
  document.body.appendChild(iframe); // necessary so the iframe contains a document
  iframe.contentDocument.body.innerHTML = input;

  function makeSanitizedCopy(node) {
    if (node.nodeType == Node.TEXT_NODE) {
      var newNode = node.cloneNode(true);
    } else if (node.nodeType == Node.ELEMENT_NODE && tagWhitelist_[node.tagName]) {
      newNode = iframe.contentDocument.createElement(node.tagName);
      for (var i = 0; i < node.attributes.length; i++) {
        var attr = node.attributes[i];
        if (attributeWhitelist_[attr.name]) {
          newNode.setAttribute(attr.name, attr.value);
        }
      }
      for (i = 0; i < node.childNodes.length; i++) {
        var subCopy = makeSanitizedCopy(node.childNodes[i]);
        newNode.appendChild(subCopy, false);
      }
    } else {
      newNode = document.createDocumentFragment();
    }
    return newNode;
  };

  var resultElement = makeSanitizedCopy(iframe.contentDocument.body);
  document.body.removeChild(iframe);
  return resultElement.innerHTML;
};

Sie können es hier ausprobieren .

Beachten Sie, dass ich in diesem Beispiel Stilattribute und Tags nicht zulasse. Wenn Sie dies zulassen, möchten Sie wahrscheinlich das CSS analysieren und sicherstellen, dass es für Ihre Zwecke sicher ist.

Ich habe dies in mehreren modernen Browsern (Chrome 40, Firefox 36 Beta, IE 11, Chrome für Android) und in einem alten Browser (IE 8) getestet, um sicherzustellen, dass es vor dem Ausführen von Skripten auf Kaution funktioniert. Es würde mich interessieren, ob es Browser gibt, die Probleme damit haben, oder Randfälle, die ich übersehen habe.

aldel
quelle
10
Dieser Beitrag verdient einige Aufmerksamkeit von Experten, da er die naheliegende und einfachste Lösung zu sein scheint. Ist es wirklich sicher?
Pwray
Wie können Sie programmgesteuert einen versteckten Iframe "mit deaktiviertem JavaScript" erstellen? Nach meinem besten Wissen ist dies unmöglich. In dem Moment, in dem Sie dies tun iframe.contentDocument.body.innerHTML = input, werden alle darin enthaltenen Skript-Tags ausgeführt.
AsGoodAsItGets
@AsGoodAsItGets - Suchen Sie das Sandbox-Attribut in iframes.
Aldel
1
@aldel In der Tat wusste ich nichts davon. Für uns ist es wegen der mangelnden Unterstützung in IE9 immer noch ein No-Go. Ich denke, Ihre Lösung könnte funktionieren, aber ich denke, Sie sollten in Ihrer Antwort klarstellen, dass Sie vom sandboxAttribut abhängen .
AsGoodAsItGets
Entschuldigung, ich dachte, das war klar aus meiner Eröffnung "Jetzt, da alle gängigen Browser Sandbox-Iframes unterstützen". Ich werde eine weniger subtile Notiz hinzufügen.
Aldel
12

Sie können nicht jede mögliche seltsame Art von fehlerhaftem Markup vorhersehen, über das ein Browser irgendwo stolpern könnte, um der schwarzen Liste zu entgehen. Es gibt viele weitere Strukturen , die Sie als nur Skript / embed / object und Handler entfernen benötigen.

Versuchen Sie stattdessen, den HTML-Code in Elemente und Attribute in einer Hierarchie zu analysieren, und führen Sie dann alle Element- und Attributnamen für eine möglichst minimale Whitelist aus. Überprüfen Sie auch alle URL-Attribute, die Sie durchgelassen haben, anhand einer Whitelist (denken Sie daran, dass es gefährlichere Protokolle als nur Javascript gibt :).

Wenn die Eingabe gut geformtes XHTML ist, ist der erste Teil der obigen Schritte viel einfacher.

Wenn Sie wie immer bei der HTML-Bereinigung einen anderen Weg finden, dies zu vermeiden, tun Sie dies stattdessen. Es gibt viele, viele mögliche Löcher. Wenn die großen Webmail-Dienste nach so vielen Jahren immer noch Exploits finden, warum glauben Sie dann, dass Sie es besser machen können?

Bobince
quelle
11

Es ist also 2016 und ich denke, viele von uns npmverwenden jetzt Module in ihrem Code. sanitize-htmlscheint die führende Option bei npm zu sein, obwohl es andere gibt .

Andere Antworten auf diese Frage liefern einen guten Beitrag dazu, wie Sie Ihre eigenen rollen können. Dies ist jedoch ein Problem, das schwierig genug ist, sodass gut getestete Community-Lösungen wahrscheinlich die beste Antwort sind.

Führen Sie dies in der Befehlszeile aus, um Folgendes zu installieren: npm install --save sanitize-html

ES5: var sanitizeHtml = require('sanitize-html'); // ... var sanitized = sanitizeHtml(htmlInput);

ES6: import sanitizeHtml from 'sanitize-html'; // ... let sanitized = sanitizeHtml(htmlInput);

ericsoco
quelle
10
2018 hier ist dies zu schwer (ein halbes Megabyte Abhängigkeiten)
user1464581
2020 hier, sanitize-html ist für Node und es gibt immer noch keine gute Option für Browser, soweit ich das beurteilen kann
Mick
3

[Haftungsausschluss: Ich bin einer der Autoren]

Wir haben dafür eine Open-Source-Bibliothek "nur Web" (dh "erfordert einen Browser") geschrieben, https://github.com/jitbit/HtmlSanitizer , die alle entfernttags/attributes/styles außer den "Whitelist" entfernt.

Verwendung:

var input = HtmlSanitizer.SanitizeHtml("<script> Alert('xss!'); </scr"+"ipt>");

PS Funktioniert viel schneller als eine "reine JavaScript" -Lösung, da der Browser zum Parsen und Bearbeiten von DOM verwendet wird. Wenn Sie an einer "reinen JS" -Lösung interessiert sind, versuchen Sie es bitte mit https://github.com/punkave/sanitize-html (nicht verbunden).

Alex
quelle
2

Die oben vorgeschlagene Google Caja-Bibliothek war viel zu komplex, um sie für eine Webanwendung zu konfigurieren und in mein Projekt aufzunehmen (also im Browser ausgeführt). Da wir bereits die CKEditor-Komponente verwenden, habe ich stattdessen auf die integrierte HTML-Bereinigungs- und Whitelist-Funktion zurückgegriffen, die viel einfacher zu konfigurieren ist. Sie können also eine CKEditor-Instanz in einen versteckten Iframe laden und Folgendes tun:

CKEDITOR.instances['myCKEInstance'].dataProcessor.toHtml(myHTMLstring)

Wenn Sie CKEditor in Ihrem Projekt nicht verwenden, ist dies möglicherweise ein Overkill, da die Komponente selbst etwa ein halbes Megabyte (minimiert) beträgt. Wenn Sie jedoch über die Quellen verfügen, können Sie den Code möglicherweise isolieren die Whitelist ( CKEDITOR.htmlParser?) und machen es viel kürzer.

http://docs.ckeditor.com/#!/api

http://docs.ckeditor.com/#!/api/CKEDITOR.htmlDataProcessor

Besser geht's nicht
quelle
0

Ich empfehle, Frameworks aus Ihrem Leben herauszuschneiden, da dies Ihnen auf lange Sicht die Dinge übermäßig erleichtern würde.

cloneNode: Das Klonen eines Knotens kopiert alle seine Attribute und deren Werte, kopiert jedoch KEINE Ereignis-Listener .

https://developer.mozilla.org/en/DOM/Node.cloneNode

Das Folgende wird nicht getestet, obwohl ich seit einiger Zeit Treewalker verwende und sie einer der am meisten unterbewerteten Teile von JavaScript sind. Hier ist eine Liste der Knotentypen, die Sie crawlen können . Normalerweise verwende ich SHOW_ELEMENT oder SHOW_TEXT .

http://www.w3.org/TR/DOM-Level-2-Traversal-Range/traversal.html#Traversal-NodeFilter

function xhtml_cleaner(id)
{
 var e = document.getElementById(id);
 var f = document.createDocumentFragment();
 f.appendChild(e.cloneNode(true));

 var walker = document.createTreeWalker(f,NodeFilter.SHOW_ELEMENT,null,false);

 while (walker.nextNode())
 {
  var c = walker.currentNode;
  if (c.hasAttribute('contentEditable')) {c.removeAttribute('contentEditable');}
  if (c.hasAttribute('style')) {c.removeAttribute('style');}

  if (c.nodeName.toLowerCase()=='script') {element_del(c);}
 }

 alert(new XMLSerializer().serializeToString(f));
 return f;
}


function element_del(element_id)
{
 if (document.getElementById(element_id))
 {
  document.getElementById(element_id).parentNode.removeChild(document.getElementById(element_id));
 }
 else if (element_id)
 {
  element_id.parentNode.removeChild(element_id);
 }
 else
 {
  alert('Error: the object or element \'' + element_id + '\' was not found and therefore could not be deleted.');
 }
}
John
quelle
5
Dieser Code setzt voraus, dass die zu bereinigende Eingabe bereits analysiert und sogar in den Dokumentbaum eingefügt wurde. In diesem Fall wurden die schädlichen Skripte bereits ausgeführt. Die Eingabe sollte eine Zeichenfolge sein.
Phihag
Senden Sie dann ein DOM-Fragment an das DOM, nur weil es sich in einer bestimmten Form oder Form im DOM befindet, bedeutet dies nicht, dass es ausgeführt wurde. Vorausgesetzt, er lädt es über AJAX, kann er dies in Verbindung mit importNode verwenden.
John