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 arbeiten ständig mit nicht vertrauenswürdigen Zeichenfolgen, aber das sichere Rendern dieser Inhalte als Teil eines HTML-Dokuments kann eine Herausforderung sein. Ohne ausreichende Sorgfalt kann es leicht passieren, dass Sie versehentlich Cross-Site-Scripting (XSS) einsetzen, die Angreifer ausnutzen könnten.

Um dieses Risiko zu minimieren, zielt der neue Vorschlag der Sanitizer API darauf ab, einen robusten Prozessor zu entwickeln, 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 mit Escapezeichen versehen

Wenn Sie Nutzereingaben, Abfragestrings, Cookie-Inhalte usw. in das DOM einfügen, müssen die Strings mit den korrekten Escapezeichen versehen werden. Besonderes Augenmerk sollte auf die DOM-Manipulation über .innerHTML gelegt werden, wobei Strings ohne Escape-Zeichen eine typische Quelle von 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 maskieren oder mit .textContent erweitern, wird alert(0) nicht ausgeführt. Da das vom Nutzer hinzugefügte <em> jedoch auch als String erweitert wird, kann diese Methode nicht verwendet werden, um die Textdekoration in HTML beizubehalten.

In diesem Fall solltest du nicht entkommen, sondern bereinigen.

Nutzereingabe bereinigen

Der Unterschied zwischen Escaping und Bereinigung

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

„Bereinigung“ bezieht sich auf das Entfernen semantisch schädlicher Teile (z. B. Skriptausführung) aus HTML-Strings.

Beispiel

Im vorherigen Beispiel führt <img onerror> dazu, dass der Fehler-Handler ausgeführt wird. Wenn der onerror-Handler jedoch entfernt würde, wäre es möglich, ihn im DOM sicher zu erweitern, 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, Tags und Attribute, die als schädlich eingestuft werden, ausgelassen und die harmlosen Tags 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 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. Es kann also wie unten dargestellt aussehen.

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

Hinweis: setHTML() ist auf Element definiert. Da es sich um eine Element-Methode 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 erweitert.

Wenn Sie das Ergebnis der Bereinigung als String abrufen 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 Konfiguration anpassen

Die Sanitizer API ist standardmäßig so konfiguriert, dass Strings entfernt werden, die die Skriptausführung auslösen. Über ein Konfigurationsobjekt können Sie jedoch auch eigene Anpassungen am Bereinigungsprozess vornehmen.

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

Die folgenden Optionen geben an, wie das Bereinigungsergebnis das angegebene Element behandeln soll.

allowElements: Namen der Elemente, die vom Desinfektionsmittel beibehalten werden sollen.

blockElements: Namen der Elemente, die mit dem Desinfektionsmittel entfernt werden sollen, ohne dass die Kinder davon betroffen sind.

dropElements: Namen der Elemente, die das Desinfektionsmittel entfernen soll, zusammen mit den Kindern.

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

Die Properties allowAttributes und dropAttributes erwarten Attributabgleichslisten, also Objekte, deren Schlüssel Attributnamen sind, und Werte sind Listen mit Zielelementen oder den Platzhalter *.

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. Sofern zulässig, 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 von DOMPurify und .innerHTML geparst. Dieses doppelte Parsen verschwendet Verarbeitungszeit, kann aber auch zu interessanten Sicherheitslücken führen, die durch Fälle entstehen, in denen das Ergebnis des zweiten Parsings vom ersten abweicht.

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

Die Sanitizer API verbessert den DOMPurify-Ansatz und wurde entwickelt, um die Notwendigkeit eines doppelten Parsens zu eliminieren und den Parsing-Kontext klarzustellen.

API-Status und Browserunterstützung

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

Schritt Status
1. Erklärende 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 on M105

Mozilla: lohnt es sich, ein Prototyping für diesen Vorschlag zu erstellen und setzt ihn aktiv um.

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

Sanitizer API aktivieren

Unterstützte Browser

  • Chrome: nicht unterstützt. <ph type="x-smartling-placeholder">
  • Edge: nicht unterstützt. <ph type="x-smartling-placeholder">
  • Firefox: hinter einer Flagge.
  • Safari: wird nicht unterstützt. <ph type="x-smartling-placeholder">

Quelle

Aktivierung über die Option about://flags oder die Befehlszeile

Chrome

Chrome implementiert derzeit 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 der Chrome Canary- und Entwicklerversion können Sie sie über --enable-blink-features=SanitizerAPI aktivieren und gleich ausprobieren. Hier finden Sie eine Anleitung dazu, wie Sie Chrome mit Flags ausführen.

Firefox

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

Funktionserkennung

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

Feedback

Wir würden uns freuen, wenn Sie diese API ausprobieren und uns Feedback geben. Teilen Sie uns Ihre Meinung zu GitHub-Problemen mit der Sanitizer API mit und diskutieren Sie mit den Autoren der Spezifikationen sowie mit Personen, die an dieser API interessiert sind.

Wenn Sie Fehler oder unerwartetes Verhalten bei der Chrome-Implementierung feststellen, melden Sie den Fehler und melden Sie ihn. Wählen Sie die Blink>SecurityFeature>SanitizerAPI-Komponenten aus und teilen Sie Details mit, damit Implementierer das Problem verfolgen 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


Foto von Towfiqu barbhuiya auf Unsplash