Sichere DOM-Manipulation mit der Sanitizer API

Jack J
Jack J

Anwendungen verarbeiten ständig nicht vertrauenswürdige Strings. Es kann jedoch schwierig sein, diese Inhalte sicher als Teil eines HTML-Dokuments zu rendern. Wenn Sie nicht ausreichend vorsichtig sind, können Sie 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 dem neuen Sanitizer API Vorschlag ein robuster Prozessor für beliebige Strings entwickelt werden, die sicher in eine Seite eingefügt werden können.

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

Nutzereingabe escapen

Wenn Sie Nutzereingaben, Abfragestrings, Cookie-Inhalte usw. in das DOM einfügen, müssen die Strings richtig escapet werden. Besondere Aufmerksamkeit sollte der DOM-Manipulation mit .innerHTML gewidmet werden, da nicht escapete 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 Eingabestring escapen oder ihn mit .textContent erweitern, wird alert(0) nicht ausgeführt. Da <em>, das vom Nutzer hinzugefügt wurde, jedoch auch als String erweitert wird, kann diese Methode nicht verwendet werden, um die Textformatierung in HTML beizubehalten.

Am besten ist es, hier nicht zu escapen, sondern zu bereinigen.

Nutzereingabe bereinigen

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

Beim Bereinigen werden semantisch schädliche Teile (z. B. die Ausführung von Skripts) 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 würde, könnte er sicher im DOM erweitert 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="">`

Um korrekt zu bereinigen, muss der Eingabestring als HTML geparst werden. Tags und Attribute, die als schädlich gelten, müssen entfernt und die harmlosen beibehalten werden.

Die vorgeschlagene Spezizerung der Sanitizer API soll eine solche Verarbeitung als Standard-API für Browser bereitstellen.

Sanitizer API

Die Sanitizer API wird so 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.

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

Es ist erwähnenswert, dass setHTML() für Element definiert ist. Da es sich um eine Methode von Element handelt, ist der zu parsende Kontext selbsterklärend (<div> in diesem Fall). Das Parsen erfolgt einmal intern und das Ergebnis wird direkt in das DOM erweitert.

Um das Ergebnis der Bereinigung als String zu erhalten, 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="">

Mit Konfiguration anpassen

Die Sanitizer API ist standardmäßig so konfiguriert, dass Strings entfernt werden, die die Ausführung von Skripts auslösen würden. Sie können den Bereinigungsprozess jedoch auch mit einem Konfigurationsobjekt anpassen.

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 angegeben, wie das Bereinigungsergebnis das angegebene Element behandeln soll.

allowElements: Namen von Elementen, die der Sanitizer beibehalten soll.

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

dropElements: Namen von Elementen, die der Sanitizer zusammen mit den untergeordneten Elementen entfernen soll.

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 steuern, ob der Sanitizer bestimmte Attribute zulässt oder ablehnt:

  • allowAttributes
  • dropAttributes

allowAttributes und dropAttributes Eigenschaften erwarten Listen mit Attributübereinstimmungen – Objekte, deren Schlüssel Attributnamen und deren Werte Listen von Zielelementen oder das * Platzhalterzeichen 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>

allowCustomElements ist die Option zum Zulassen oder Ablehnen benutzerdefinierter Elemente. 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, die Bereinigungsfunktionen bietet. Der Hauptunterschied zwischen der Sanitizer API und DOMPurify besteht darin, dass DOMPurify das Ergebnis der Bereinigung als String zurückgibt, den Sie mit .innerHTML in ein DOM-Element schreiben müssen.

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 DOMPurify-Implementierung hat einige Nachteile. Wenn ein String zurückgegeben wird, wird der Eingabestring zweimal geparst, von DOMPurify und .innerHTML. Dieses doppelte Parsen verschwendet Rechenzeit, kann aber auch zu interessanten Sicherheitslücken führen, wenn sich das Ergebnis des zweiten Parsens vom ersten unterscheidet.

HTML muss auch Kontext haben, um geparst zu werden. Beispielsweise ist <td> in <table> sinnvoll, aber nicht in <div>. Da DOMPurify.sanitize() nur einen String als Argument akzeptiert, musste der Parsing-Kontext erraten werden.

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

API-Status und Browserunterstützung

Die Sanitizer API wird im Standardisierungsprozess diskutiert und Chrome implementiert sie gerade.

Schritt Status
1. Erläuterung erstellen Abgeschlossen
2. Spezifikationsentwurf erstellen Abgeschlossen
3. Feedback einholen und Design optimieren Abgeschlossen
4. Chrome-Ursprungstest Abgeschlossen
5. Starten Versandabsicht für M105

Mozilla: Hält diesen Vorschlag für einen Prototyp wert und implementiert ihn aktiv.

WebKit: Die Antwort finden Sie in der Mailingliste von WebKit.

Sanitizer API aktivieren

Browser Support

  • Chrome: 146.
  • Edge: 146.
  • Firefox: 148.
  • Safari: not supported.

Chrome implementiert gerade die Sanitizer API. 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 der Entwicklerversion können Sie es mit --enable-blink-features=SanitizerAPI aktivieren. Eine Anleitung zum Ausführen von Chrome mit Flags finden Sie hier: instructions for how to run Chrome with flags.

Firefox

Firefox implementiert die Sanitizer API auch als experimentelle Funktion. Setzen Sie dazu das Flag dom.security.sanitizer.enabled in about:config auf true.

Funktionserkennung

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

Feedback

Wenn Sie diese API testen und Feedback haben, freuen wir uns darauf, von Ihnen zu hören. Teilen Sie uns Ihre Meinung zu den GitHub-Problemen der Sanitizer API mit und diskutieren Sie mit den Autoren der Spezifikation und anderen, die sich für diese API interessieren.

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

Demo

Wenn Sie die Sanitizer API in Aktion sehen möchten, sehen Sie sich den Sanitizer API Playground von Mike West an:

Verweise