Zapobieganie atakom typu XSS opartym na DOM przy użyciu dyrektywy Trusted Types

Funkcje internetowe: Zaufane typy

Zaufane typy to funkcja zabezpieczeń, która pomaga zapobiegać atakom typu cross-site scripting opartym na DOM (DOM XSS). Wymaga ona, aby dane przekazywane do „niebezpiecznych” interfejsów API przeglądarki (nazywanych ujściami) były najpierw konwertowane na określony typ, np. TrustedHTML.

Domyślnie wiele interfejsów API DOM (takich jak innerHTML lub document.write) akceptuje nieprzetworzone ciągi znaków. Jeśli ciąg znaków zawiera szkodliwy skrypt z niezaufanego źródła, może on zostać wykonany. Zaufane typy zmieniają to zachowanie: po włączeniu za pomocą CSP te ujścia będą akceptować tylko obiekty TrustedHTML utworzone przez autoryzowane zasady.

Najważniejsze szczegóły implementacji

Aby używać typów zaufanych, wykonaj 4 główne czynności:

  1. Zdefiniuj zasadę zabezpieczeń, która określa, jak czyścić lub przekształcać ciągi znaków.
  2. Użyj tej zasady, aby utworzyć obiekty typu Trusted Type z nieprzetworzonych ciągów znaków.
  3. Podczas zapisywania w elementach DOM używaj obiektów typu Trusted Type.
  4. Wymuszaj stosowanie typów zaufanych za pomocą standardu Content Security Policy (CSP).

1. Tworzenie zasady

Zasada to zestaw reguł dotyczących usuwania informacji z treści. Tworzy się go za pomocą trustedTypes.createPolicy(), zwykle delegując do biblioteki czyszczącej, takiej jak DOMPurify, lub reguł zmiany znaczenia kodu HTML aplikacji:

import DOMPurify from 'dompurify';

const myPolicy = trustedTypes.createPolicy('sanitize-html', {
  createHTML: (input) =>
    // Return a sanitized string; createPolicy() wraps it in a TrustedHTML object.
    // Example using DOMPurify:
    DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: false }),
});

// Example using HTML escaping (for environments without third-party sanitizers):
const escapePolicy = trustedTypes.createPolicy('escape-html', {
  createHTML: (input) =>
    String(input)
      .replaceAll('&', '&')
      .replaceAll('<', '&lt;')
      .replaceAll('>', '&gt;')
      .replaceAll('"', '&quot;')
      .replaceAll("'", '&#39;'),
});

2. Korzystanie z zasad

Zamiast przekazywać ciąg znaków bezpośrednio do ujścia, wywołujesz metodę tworzenia zasady.

const untrustedInput = '<p>Hello, <strong>world</strong>!</p><img src="x" onerror="alert(1)">';

// In an enforced environment, this would throw a TypeError:
// element.innerHTML = untrustedInput;

// This returns a TrustedHTML object with unsafe elements and attributes removed:
const cleanHTML = myPolicy.createHTML(untrustedInput);
// Result: "<p>Hello, <strong>world</strong>!</p><img src=\"x\">"

3. Przenoszenie ujść w celu używania typów zaufanych

Zaktualizuj każde wystąpienie, w którym zapisujesz dane w ujściu, aby najpierw przekonwertować je na obiekt typu zaufanego. Aby ułatwić znalezienie miejsca, w którym należy zaktualizować kod, możesz zdefiniować tymczasową zasadę default, która rejestruje, kiedy do ujścia jest zapisywany ciąg znaków, a nie TrustedType.

trustedTypes.createPolicy("default", {
  createHTML(value) {
    console.warn("String passed instead of Trusted Type.");
    return DOMPurify.sanitize(value, { RETURN_TRUSTED_TYPE: false });
  },
});

Typowe ujścia obsługujące typy zaufane:

  • HTML: element.innerHTML, element.outerHTML, document.write(), element.setHTMLUnsafe(), Document.parseHTMLUnsafe()
  • Skrypt: <script> treść tekstowa, eval()
  • ScriptURL: <script src>, Worker(), SharedWorker()
// Assign the TrustedHTML object.
element.innerHTML = cleanHTML;

Pamiętaj, że „bezpieczne” metody czyszczenia kodu HTML (element.setHTML() i Document.parseHTML(), opisane w artykule Czyszczenie niezaufanego kodu HTML) nie wymagają zasad dotyczących typów zaufanych, ponieważ zawsze natywnie czyszczą potencjalne ataki XSS.

4. Egzekwowanie dyrektywy Trusted Types za pomocą CSP

Typy zaufane należy wdrażać stopniowo, aby uniknąć uszkodzenia aplikacji. Pełne egzekwowanie zapewnia najsilniejszą ochronę, ale wiąże się z ryzykiem uszkodzenia witryny, jeśli nie wszystkie zapisy w ujściu zostały przeniesione.

Wymagania wstępne

  • Współpraca z platformą: jeśli platforma aplikacji (lub dowolna biblioteka innej firmy, która zapisuje dane w elementach DOM) nie generuje wartości TrustedHTML ani TrustedScript, nie możesz wymusić stosowania typów zaufanych bez uszkodzenia tego kodu. Przed rozpoczęciem wdrażania sprawdź obsługę platformy audytu.
  • Refaktoryzacja ujść: upewnij się, że niebezpieczne ujścia zostały zidentyfikowane i zrefaktoryzowane, aby używać zasad typów zaufanych.

Strategia wdrażania

  1. Tylko raportowanie: najpierw wdróż nagłówek Content-Security-Policy-Report-Only. Spowoduje to rejestrowanie naruszeń w punkcie końcowym raportowania bez blokowania zapisu.

    Content-Security-Policy-Report-Only: require-trusted-types-for 'script';
    
  2. Egzekwowanie: po zintegrowaniu zasad i wyeliminowaniu naruszeń przejdź do pełnego egzekwowania.

    Content-Security-Policy: require-trusted-types-for 'script';
    

Możesz też ograniczyć tworzenie zasad (wymień nazwy zasad, np. sanitize-html lub escape-html, uwzględnij dompurify, jeśli zasada przekazuje uprawnienia do DOMPurify.sanitize(), co powoduje utworzenie zasady dompurify, i uwzględnij default, jeśli podczas migracji rejestrujesz zasadę domyślną):

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types sanitize-html escape-html dompurify;

Po zastosowaniu tej zasady CSP każdy zapis do ujścia, który nie korzysta z typów zaufanych, spowoduje błąd. Dodatkowo próba utworzenia zasady Trusted Type o nazwie, która nie jest wymieniona w trusted-types, spowoduje błąd.

Możesz też ustawić te zasady CSP jako tag <meta> w sekcji <head> dokumentu, ale może to być mniej bezpieczne.

<!-- First child of `<head>`, as earlier tags will not enforce this CSP. -->
<meta
  http-equiv="Content-Security-Policy"
  content="require-trusted-types-for 'script'; trusted-types sanitize-html escape-html dompurify;"
/>

Strategie zastępcze

W przypadku przeglądarek, które nie obsługują jeszcze typów zaufanych, możesz użyć minimalnego kodu „tinyfill”, aby zapewnić, że kod nie zostanie uszkodzony, a jednocześnie zapewnić oczyszczanie danych.

if (!window.trustedTypes) {
  window.trustedTypes = {
    createPolicy: (name, rules) => rules,
  };
}

Dzięki temu możesz już dziś pisać kod, korzystając ze wzorca Trusted Types. Obsługiwane przeglądarki będą wymuszać typy, a starsze przeglądarki będą nadal wykonywać logikę czyszczenia danych za pomocą zasady próbnej.

Jeśli przeglądarka nie obsługuje zaufanych typów, będzie traktować obiekty TrustedHTML jako ciągi znaków (jeśli używasz tinyfill) lub nie będzie przeprowadzać kontroli egzekwowania na poziomie ujścia.