Sichere DOM-Manipulation mit der Sanitizer API

Ziel der neuen Sanitizer API ist es, einen robusten Prozessor zu entwickeln, mit dem beliebige Strings sicher in eine Seite eingefügt werden können.

Jack J
Jack J

Anwendungen müssen ständig mit nicht vertrauenswürdigen Strings umgehen, aber das sichere Rendern dieser Inhalte als Teil eines HTML-Dokuments kann schwierig sein. Ohne ausreichende Sorgfalt können Sie leicht versehentlich Möglichkeiten für Cross-Site-Scripting (XSS) schaffen, die von böswilligen Angreifern ausgenutzt werden können.

Um dieses Risiko zu minimieren, soll mit der neuen Sanitizer API ein robuster Prozessor entwickelt werden, mit dem beliebige Strings sicher in eine Seite eingefügt werden können. In diesem Artikel wird die API vorgestellt und ihre Verwendung erläutert.

// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerror=alert(0)>`, new Sanitizer())

Nutzereingabe escapen

Wenn Nutzereingaben, Suchstrings, Cookie-Inhalte usw. in das DOM eingefügt werden, müssen die Strings korrekt entkommentiert werden. Besonderes Augenmerk sollte auf die DOM-Manipulation über .innerHTML gelegt werden, da nicht entescapede Strings eine typische Quelle für XSS sind.

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.innerHTML = user_input

Wenn Sie HTML-Sonderzeichen im obigen Eingabestring mit einem Escape-Zeichen versehen oder ihn mit .textContent erweitern, wird alert(0) nicht ausgeführt. Da vom Nutzer hinzugefügtes <em> jedoch auch als String erweitert wird, kann diese Methode nicht verwendet werden, um die Textdekoration in HTML beizubehalten.

Am besten ist es, die Zeichen nicht zu entkommenttieren, sondern zu bereinigen.

Nutzereingabe bereinigen

Der Unterschied zwischen Entkommentierung und Sanitieren

Beim Entkommenttieren werden spezielle HTML-Zeichen durch HTML-Entitäten ersetzt.

Bei der Bereinigung werden semantisch schädliche Teile (z. B. Scriptausführung) aus HTML-Strings entfernt.

Beispiel

Im vorherigen Beispiel führt <img onerror> dazu, dass der Fehlerhandler ausgeführt wird. Wenn der onerror-Handler jedoch entfernt wird, kann er im DOM sicher maximiert werden, während <em> intakt bleibt.

// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerror=alert(0)>`
// Sanitized ⛑
$div.innerHTML = `<em>hello world</em><img src="">`

Für eine korrekte Bereinigung muss der Eingabestring als HTML geparst werden. Dabei müssen schädliche Tags und Attribute entfernt und harmlose beibehalten werden.

Die vorgeschlagene Sanitizer API-Spezifikation zielt darauf ab, eine solche Verarbeitung als Standard-API für Browser bereitzustellen.

Sanitizer API

Die Sanitizer API wird folgendermaßen verwendet:

const $div = document.querySelector('div')
const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.setHTML(user_input, { sanitizer: new Sanitizer() }) // <div><em>hello world</em><img src=""></div>

{ sanitizer: new Sanitizer() } ist jedoch das Standardargument. Das könnte so aussehen:

$div.setHTML(user_input) // <div><em>hello world</em><img src=""></div>

Hinweis: setHTML() ist auf Element definiert. Da es sich um eine Methode von Element handelt, ist der zu parsende Kontext selbsterklärend (in diesem Fall <div>). Das Parsen erfolgt einmal intern und das Ergebnis wird direkt in das DOM eingefügt.

Wenn Sie das Ergebnis der Bereinigung als String erhalten möchten, können Sie .innerHTML aus den setHTML()-Ergebnissen verwenden.

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.innerHTML // <em>hello world</em><img src="">

Über die Konfiguration anpassen

Die Sanitizer API ist standardmäßig so konfiguriert, dass Strings entfernt werden, die die Scriptausführung auslösen würden. Sie können dem Desinfektionsprozess aber auch über ein Konfigurationsobjekt eigene Anpassungen hinzufügen.

const config = {
  allowElements: [],
  blockElements: [],
  dropElements: [],
  allowAttributes: {},
  dropAttributes: {},
  allowCustomElements: true,
  allowComments: true
};
// sanitized result is customized by configuration
new Sanitizer(config)

Mit den folgenden Optionen wird festgelegt, wie das angegebene Element im Ergebnis der Bereinigung behandelt werden soll.

allowElements: Namen von Elementen, die vom Sanitizer beibehalten werden sollen.

blockElements: Namen der Elemente, die der Sanitizer entfernen soll, während die untergeordneten Elemente beibehalten werden.

dropElements: Namen der Elemente, die der Sanitizer entfernen soll, zusammen mit ihren untergeordneten Elementen.

const str = `hello <b><i>world</i></b>`

$div.setHTML(str)
// <div>hello <b><i>world</i></b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: [ "b" ]}) })
// <div>hello <b>world</b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ "b" ]}) })
// <div>hello <i>world</i></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: []}) })
// <div>hello world</div>

Mit den folgenden Optionen können Sie auch festlegen, ob bestimmte Attribute vom Sanitizer zugelassen oder abgelehnt werden:

  • allowAttributes
  • dropAttributes

Für allowAttributes- und dropAttributes-Properties sind Listen mit Attributübereinstimmungen erforderlich. Das sind Objekte, deren Schlüssel Attributnamen und deren Werte Listen von Zielelementen oder der Platzhalter * sind.

const str = `<span id=foo class=bar style="color: red">hello</span>`

$div.setHTML(str)
// <div><span id="foo" class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["span"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["p"]}}) })
// <div><span>hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["*"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({dropAttributes: {"id": ["span"]}}) })
// <div><span class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// <div>hello</div>

Mit allowCustomElements können Sie benutzerdefinierte Elemente zulassen oder ablehnen. Wenn sie zulässig sind, gelten weiterhin andere Konfigurationen für Elemente und Attribute.

const str = `<custom-elem>hello</custom-elem>`

$div.setHTML(str)
// <div></div>

const sanitizer = new Sanitizer({
  allowCustomElements: true,
  allowElements: ["div", "custom-elem"]
})
$div.setHTML(str, { sanitizer })
// <div><custom-elem>hello</custom-elem></div>

API-Oberfläche

Vergleich mit DomPurify

DOMPurify ist eine bekannte Bibliothek mit Funktionen zur Bereinigung. Der Hauptunterschied zwischen der Sanitizer API und DOMPurify besteht darin, dass DOMPurify das Ergebnis der Bereinigung als String zurückgibt, den du über .innerHTML in ein DOM-Element schreiben musst.

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sanitized
// `<em>hello world</em><img src="">`

DOMPurify kann als Fallback dienen, wenn die Sanitizer API nicht im Browser implementiert ist.

Die Implementierung von DOMPurify hat einige Nachteile. Wenn ein String zurückgegeben wird, wird der Eingabestring zweimal geparst: von DOMPurify und .innerHTML. Dieses doppelte Parsen verschwendet Verarbeitungszeit, kann aber auch zu interessanten Sicherheitslücken führen, wenn sich das Ergebnis des zweiten Parsens vom ersten unterscheidet.

Für das Parsen von HTML ist außerdem Kontext erforderlich. Beispiel: <td> ist in <table> sinnvoll, aber nicht in <div>. Da DOMPurify.sanitize() nur einen String als Argument akzeptiert, musste der Parsekontext erraten werden.

Die Sanitizer API verbessert den DOMPurify-Ansatz und soll das doppelte Parsen überflüssig machen und den Parsekontext verdeutlichen.

API-Status und Browserunterstützung

Die Sanitizer API wird derzeit im Rahmen des Standardisierungsprozesses diskutiert und in Chrome implementiert.

Schritt Status
1. Erläuternde Mitteilung erstellen Abschließen
2. Spezifikationsentwurf erstellen Abschließen
3. Feedback einholen und Design iterieren Abschließen
4. Chrome-Ursprungstest Abschließen
5. Starten Intent to Ship auf M105

Mozilla: Dieser Vorschlag lohnt sich für einen Prototyp und wird aktiv implementiert.

WebKit: Antwort in der WebKit-Mailingliste

Sanitizer API aktivieren

Über about://flags oder die Befehlszeilenoption aktivieren

Chrome

Die Sanitizer API wird derzeit in Chrome implementiert. In Chrome 93 oder höher können Sie das Verhalten testen, indem Sie das Flag about://flags/#enable-experimental-web-platform-features aktivieren. In früheren Versionen von Chrome Canary und im Dev-Kanal können Sie die Funktion über --enable-blink-features=SanitizerAPI aktivieren und sofort ausprobieren. Informationen zum Ausführen von Chrome mit Flags

Firefox

Firefox implementiert die Sanitizer API ebenfalls als experimentelle Funktion. Wenn Sie die Funktion aktivieren möchten, setzen Sie das dom.security.sanitizer.enabled-Flag in about:config auf true.

Funktionserkennung

if (window.Sanitizer) {
  // Sanitizer API is enabled
}

Feedback

Wenn Sie diese API ausprobieren und Feedback dazu haben, würden wir uns sehr darüber freuen. Teilen Sie uns Ihre Meinung zu den GitHub-Problemen der Sanitizer API mit und diskutieren Sie mit den Autoren der Spezifikation und anderen Interessierten an dieser API.

Wenn Sie Fehler oder unerwartetes Verhalten in der Chrome-Implementierung feststellen, melden Sie sie bitte. Wählen Sie die Blink>SecurityFeature>SanitizerAPI-Komponenten aus und teilen Sie Details mit, damit die Implementierer das Problem nachvollziehen können.

Demo

Die Sanitizer API in Aktion sehen Sie im Sanitizer API Playground von Mike West:

Verweise


Foto von Towfiqu barbhuiya auf Unsplash.