Sichere DOM-Manipulation mit der Sanitizer API

Ziel der neuen Sanitizer API ist es, einen robusten Prozessor für beliebige Strings aufzubauen, die sicher in eine Seite eingefügt werden können.

Jack J
Jack J

Anwendungen haben ständig mit nicht vertrauenswürdigen Zeichenfolgen zu tun, aber es kann schwierig sein, diese Inhalte als Teil eines HTML-Dokuments sicher zu rendern. Ohne ausreichende Sorgfalt kann es leicht passieren, dass man versehentlich Möglichkeiten für Cross-Site-Scripting (XSS) schafft, die von böswilligen Angreifern ausgenutzt werden könnten.

Um dieses Risiko zu verringern, zielt der neue Vorschlag der Sanitizer API darauf ab, einen robusten Prozessor für beliebige Strings zu erstellen, die sicher in eine Seite eingefügt werden können. In diesem Artikel erhalten Sie eine Einführung in die API und ihre Verwendung.

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

Maskierung der Nutzereingabe

Wenn Sie Nutzereingaben, Abfragestrings, Cookie-Inhalte usw. in das DOM einfügen, müssen die Strings korrekt mit Escapezeichen versehen werden. Besonderes Augenmerk sollte auf DOM-Manipulation über .innerHTML gelegt werden, bei der nicht maskierte Strings eine typische XSS-Quelle sind.

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

Wenn Sie im Eingabestring oben HTML-Sonderzeichen als Escape-Zeichen verwenden oder ihn mit .textContent erweitern, wird alert(0) nicht ausgeführt. Da der vom Nutzer hinzugefügte <em> jedoch unverändert als String erweitert wird, kann diese Methode nicht verwendet werden, um die Textformatierung in HTML beizubehalten.

Das Beste, was Sie hier tun sollten, ist nicht Escaping, sondern desinfizieren.

Nutzereingabe bereinigen

Der Unterschied zwischen Escaping und Desinfektion

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

Bereinigung bedeutet, semantisch schädliche Teile (z. B. die Skriptausführung) aus HTML-Strings zu entfernen.

Beispiel

Im vorherigen Beispiel sorgt <img onerror> dafür, dass der Fehler-Handler ausgeführt wird. Wenn der onerror-Handler jedoch entfernt würde, wäre es möglich, ihn sicher im DOM 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 ist es erforderlich, den Eingabestring als HTML zu parsen, als schädlich eingestufte Tags und Attribute auszulassen und die harmlosen zu behalten.

Die vorgeschlagene Sanitizer API-Spezifikation zielt darauf ab, eine solche Verarbeitung wie eine Standard-API für Browser zu ermöglichen.

Desitizer-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. Sie kann also wie unten aussehen.

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

Beachten Sie, dass setHTML() in Element definiert ist. Bei der Methode Element ist der zu parsende Kontext selbsterklärend (in diesem Fall <div>). Das Parsing wird intern einmal durchgeführt und das Ergebnis wird direkt in das DOM erweitert.

Um das Ergebnis der Bereinigung als String abzurufen, 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 würden. Sie können dem Bereinigungsprozess jedoch auch eigene Anpassungen über ein Konfigurationsobjekt 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 Bereinigungsergebnis verarbeitet werden soll.

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

blockElements: Namen der Elemente, die vom Desinfektionsmittel entfernt werden sollen, aber die Kinder beibehalten.

dropElements: Die Namen der Elemente, die vom Desinfektionsmittel entfernt werden sollen, sowie die zugehörigen untergeordneten Elemente.

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 Desinfektionsmittel zugelassen oder abgelehnt werden:

  • allowAttributes
  • dropAttributes

Die Attribute allowAttributes und dropAttributes erwarten Listen mit Attributabgleichslisten – Objekte, deren Schlüssel Attributnamen und Werte Listen von Zielelementen oder dem 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 festlegen, ob benutzerdefinierte Elemente zugelassen oder abgelehnt werden. Wenn sie zulässig sind, gelten andere Konfigurationen für Elemente und Attribute weiterhin.

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 über .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. Wird ein String zurückgegeben, wird er zweimal über 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 sich das Ergebnis des zweiten Parsings vom ersten unterscheidet.

HTML benötigt außerdem Kontext, um geparst zu werden. Beispielsweise ist <td> in <table> sinnvoll, aber nicht in <div>. Da DOMPurify.sanitize() nur einen String als Argument verwendet, musste der Parsing-Kontext erraten werden.

Die Sanitizer API verbessert den DOMPurify-Ansatz und wurde entwickelt, um doppeltes Parsing zu vermeiden und den Parsing-Kontext zu verdeutlichen.

API-Status und Browserunterstützung

Die Sanitizer API wird derzeit im Standardisierungsprozess erörtert und in Chrome implementiert.

Step Status
1. Erklärende Erklärung erstellen Abschließen
2. Spezifikationsentwurf erstellen Abschließen
3. Feedback einholen und Design iterieren Abschließen
4. Chrome-Ursprungstest Abschließen
5. Launch Versandabsicht M105

Mozilla: betrachtet dieses Angebot als Prototyping und führt es aktiv um.

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

So aktivieren Sie die Sanitizer API

Unterstützte Browser

  • x
  • x
  • x

Quelle

Aktivierung über about://flags oder Befehlszeilenoption

Chrome

Die Sanitizer API wird derzeit in Chrome implementiert. In Chrome 93 oder höher können Sie das Verhalten ausprobieren, indem Sie das Flag about://flags/#enable-experimental-web-platform-features aktivieren. In früheren Versionen von Chrome Canary und der Entwicklerversion kannst du sie über --enable-blink-features=SanitizerAPI aktivieren und gleich ausprobieren. Lesen Sie die Anleitung zum Ausführen von Chrome mit Flags.

Firefox

Firefox implementiert außerdem die Sanitizer API als experimentelle Funktion. Zum Aktivieren setzen Sie das Flag dom.security.sanitizer.enabled in about:config auf true.

Funktionserkennung

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

Feedback

Wenn Sie diese API ausprobieren und Feedback haben, freuen wir uns. Teilen Sie Ihre Gedanken zu Sanitizer API GitHub-Problemen mit und sprechen Sie mit den Spezifikationsautoren und den an dieser API interessierten Personen.

Wenn Sie Fehler oder unerwartetes Verhalten in der Implementierung von Chrome feststellen, melden Sie dies bitte in einem Fehlerbericht. Wählen Sie die Blink>SecurityFeature>SanitizerAPI-Komponenten aus und teilen Sie sie mit, um den Implementierern zu helfen, das Problem zu verfolgen.

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.