Zapobiegaj lukom w zabezpieczeniach witryn opartych na DOM, korzystając z zaufanych typów

Krzysztof Kotowicz
Krzysztof Kotowicz

Obsługa przeglądarek

  • Chrome: 83.
  • Edge: 83.
  • Firefox: nieobsługiwane.
  • Safari: nieobsługiwane.

Źródło

Skryptowanie między witrynami oparte na DOM (DOM XSS) występuje, gdy dane ze źródła kontrolowanego przez użytkownika (np. nazwy użytkownika lub adresu URL przekierowania pobranego z fragmentu adresu URL) docierają do sinka, czyli funkcji takiej jak eval() lub settera właściwości takiej jak .innerHTML, który może wykonać dowolny kod JavaScript.

Atak XSS w DOM jest jedną z najczęstszych luk w zabezpieczeniach witryn internetowych i często jest przypadkowo wprowadzany przez zespoły programistów do aplikacji. Zaufane typy zapewniają narzędzia do pisania, sprawdzania zabezpieczeń i unikania podatności na ataki XSS w DOM, ponieważ niebezpieczne funkcje interfejsów API są domyślnie zabezpieczone. Zaufane typy są dostępne jako polyfill w przeglądarkach, które jeszcze ich nie obsługują.

Tło

Przez wiele lat XSS w DOM był jedną z najczęstszych i najniebezpieczniejszych luk w zabezpieczeniach sieci.

Istnieją 2 rodzaje ataków typu cross-site scripting. Niektóre podatności XSS są spowodowane kodem po stronie serwera, który w niebezpieczny sposób tworzy kod HTML tworzący witrynę. Inne mają przyczynę po stronie klienta, gdzie kod JavaScript wywołuje niebezpieczne funkcje z zawartością kontrolowaną przez użytkownika.

Aby zapobiec XSS po stronie serwera, nie generuj kodu HTML przez konkatenację ciągów znaków. Aby dodatkowo ograniczyć występowanie błędów, użyj bezpiecznych bibliotek szablonów z automatycznym osadzaniem w kontekście oraz zasad bezpieczeństwa treści opartych na szyfrach.

Teraz przeglądarki mogą też zapobiegać atakom XSS po stronie klienta na podstawie DOM-u, korzystając z zaufanych typów.

Wprowadzenie do interfejsów API

Zaufane typy działają poprzez zablokowanie tych niebezpiecznych funkcji. Niektóre z nich możesz już znać, ponieważ producenci przeglądarek i ramy sieciowe odradzają już używanie tych funkcji ze względów bezpieczeństwa.

Zaufane typy wymagają przetworzenia danych przed przekazaniem ich do funkcji sink. Użycie tylko ciągu tekstowego spowoduje niepowodzenie, ponieważ przeglądarka nie wie, czy dane są wiarygodne:

Nie
anElement.innerHTML  = location.href;
Jeśli masz włączone zaufane typy, przeglądarka zwraca błąd TypeError i nie pozwala używać elementu DOM XSS sink z łańcuchem znaków.

Aby wskazać, że dane zostały przetworzone bezpiecznie, utwórz specjalny obiekt – zaufany typ.

Tak
anElement.innerHTML = aTrustedHTML;
  
Gdy włączone są zaufane typy, przeglądarka akceptuje obiekt TrustedHTML dla odbiorników, które oczekują fragmentów kodu HTML. Dostępne są też obiekty TrustedScriptTrustedScriptURL do innych wrażliwych zlewozmywaków.

Zaufane typy znacznie zmniejszają powierzchnię ataku XSS w DOM-ie Twojej aplikacji. Ułatwia przeglądy zabezpieczeń i umożliwia egzekwowanie kontroli bezpieczeństwa opartych na typie podczas kompilowania, sprawdzania kodu lub tworzenia pakietu kodu w czasie wykonywania w przeglądarce.

Jak korzystać z zaufanych typów

Przygotowanie do raportów o naruszeniu zasad Content Security Policy

Możesz wdrożyć narzędzie do zbierania raportów, np. opensource’owe reporting-api-processor lub go-csp-collector, albo użyć jednego z komercyjnych odpowiedników. Możesz też dodać niestandardowe logowanie i debugowanie naruszeń w przeglądarce za pomocą klasy ReportingObserver:

const observer = new ReportingObserver((reports, observer) => {
    for (const report of reports) {
        if (report.type !== 'csp-violation' ||
            report.body.effectiveDirective !== 'require-trusted-types-for') {
            continue;
        }

        const violation = report.body;
        console.log('Trusted Types Violation:', violation);

        // ... (rest of your logging and reporting logic)
    }
}, { buffered: true });

observer.observe();

lub dodać detektor zdarzeń:

document.addEventListener('securitypolicyviolation',
    console.error.bind(console));

Dodawanie nagłówka CSP tylko do raportowania

Dodaj ten nagłówek odpowiedzi HTTP do dokumentów, które chcesz przenieść do zaufanych typów:

Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example

Teraz wszystkie naruszenia są zgłaszane do //my-csp-endpoint.example, ale strona nadal działa. W następnej sekcji wyjaśniamy, jak działa //my-csp-endpoint.example.

Identyfikowanie naruszeń zasad dotyczących zaufanych typów

Od tej pory za każdym razem, gdy Trusted Types wykryje naruszenie, przeglądarka będzie wysyłać raport do skonfigurowanego report-uri. Jeśli np. aplikacja przekaże ciąg znaków do funkcji innerHTML, przeglądarka wyśle taki raport:

{
"csp-report": {
    "document-uri": "https://my.url.example",
    "violated-directive": "require-trusted-types-for",
    "disposition": "report",
    "blocked-uri": "trusted-types-sink",
    "line-number": 39,
    "column-number": 12,
    "source-file": "https://my.url.example/script.js",
    "status-code": 0,
    "script-sample": "Element innerHTML <img src=x"
}
}

Z tego wynika, że w pliku https://my.url.example/script.js na linii 39 wywołano funkcję innerHTML z ciągiem znaków zaczynającym się od <img src=x. Te informacje powinny pomóc Ci określić, które części kodu mogą powodować atak XSS w DOM i wymagają zmiany.

Napraw naruszenia

Naruszenie zasad dotyczących zaufanego typu można naprawić na kilka sposobów. Możesz usunąć kod, który narusza zasady, użyć biblioteki, utworzyć zasadę typu zaufany lub, jako ostateczność, utworzyć zasadę domyślną.

Zmień kod powodujący problem.

Możliwe, że kod niezgodny z zasadami nie jest już potrzebny lub można go przepisać bez funkcji, które powodują naruszenia:

Tak
el.textContent = '';
const img = document.createElement('img');
img.src = 'xyz.jpg';
el.appendChild(img);
Nie
el.innerHTML = '<img src=xyz.jpg>';

Korzystanie z biblioteki

Niektóre biblioteki generują już zaufane typy, które możesz przekazywać do funkcji sink. Możesz na przykład użyć narzędzia DOMPurify do odizolowania fragmentu kodu HTML i usunięcia z niego ładunków XSS.

import DOMPurify from 'dompurify';
el.innerHTML = DOMPurify.sanitize(html, {RETURN_TRUSTED_TYPE: true});

DOMPurify obsługuje zaufane typy i zwraca oczyszczony kod HTML zapakowany w obiekt TrustedHTML, aby przeglądarka nie generowała naruszenia.

Tworzenie zasad dotyczących zaufanych typów

Czasami nie można usunąć kodu, który powoduje naruszenie, i nie ma biblioteki, która mogłaby wyczyścić wartość i utworzyć zaufany typ. W takich przypadkach możesz samodzielnie utworzyć obiekt zaufany.

Najpierw utwórz zasady. Zasady to fabryki zaufanych typów, które stosują określone reguły zabezpieczeń na swoich danych wejściowych:

if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
  const escapeHTMLPolicy = trustedTypes.createPolicy('myEscapePolicy', {
    createHTML: string => string.replace(/\</g, '&lt;')
  });
}

Ten kod tworzy zasadę o nazwie myEscapePolicy, która może generować obiekty TrustedHTML za pomocą funkcji createHTML(). Zdefiniowane reguły HTML-escape < znaków, aby zapobiec tworzeniu nowych elementów HTML.

Użyj tej zasady:

const escaped = escapeHTMLPolicy.createHTML('<img src=x onerror=alert(1)>');
console.log(escaped instanceof TrustedHTML);  // true
el.innerHTML = escaped;  // '&lt;img src=x onerror=alert(1)>'

Używanie domyślnej zasady

Czasami nie możesz zmienić kodu, który powoduje problemy, na przykład gdy wczytujesz bibliotekę zewnętrzną z CDN. W takim przypadku użyj zasad domyślnych:

if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
  trustedTypes.createPolicy('default', {
    createHTML: (string, sink) => DOMPurify.sanitize(string, {RETURN_TRUSTED_TYPE: true})
  });
}

Zasady o nazwie default są używane wszędzie tam, gdzie w zbiorze danych jest używany ciąg znaków, który akceptuje tylko zaufany typ.

Przełączanie na egzekwowanie standardu Content Security Policy

Gdy aplikacja przestanie generować naruszenia, możesz zacząć egzekwować zasady dotyczące zaufanych typów:

Content-Security-Policy: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example

Niezależnie od tego, jak złożona jest Twoja aplikacja internetowa, jedyną rzeczą, która może spowodować podatność na atak XSS w DOM, jest kod w jednej z zasad. Możesz jeszcze bardziej zablokować tę funkcję, ograniczając tworzenie zasad.

Więcej informacji