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 to narzędzia do pisania i sprawdzania zabezpieczeń oraz do ochrony aplikacji przed podatnością na ataki XSS w DOM, które są domyślnie zabezpieczone dzięki niebezpiecznym funkcjom interfejsu API. 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 luki XSS są powodowane przez kod po stronie serwera, który w niebezpieczny sposób tworzy kod HTML tworzący witrynę. Inne mają pierwotną przyczynę po stronie klienta, gdzie kod JavaScript wywołuje niebezpieczne funkcje z zawartością kontrolowaną przez użytkownika.

Aby zapobiec atakom XSS po stronie serwera, nie generuj kodu HTML przez łączenie ciągów znaków. Użyj zamiast tego bezpiecznych bibliotek tworzenia szablonów z automatycznym stosowaniem kontekstu oraz zasady Content Security Policy, które umożliwiają dodatkowe łagodzenie błędów.

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ą, blokując poniższe ryzykowne funkcje ujścia. Niektóre z nich możesz już znać, ponieważ producenci przeglądarek i ramy sieciowe odradzają używania tych funkcji ze względów bezpieczeństwa.

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

Nie
anElement.innerHTML  = location.href;
Gdy włączone są zaufane typy, przeglądarka zgłasza TypeError i uniemożliwia użycie ujścia DOM XSS z ciągiem 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. Istnieją też obiekty TrustedScript i TrustedScriptURL dla innych wrażliwych ujść.

Zaufane typy znacznie zmniejszają obszar, na którym można przeprowadzić atak XSS w DOM-ie, w Twojej aplikacji. Upraszcza ona kontrole zabezpieczeń i umożliwia wymuszanie kontroli bezpieczeństwa zależnych od typu, które są przeprowadzane podczas kompilowania, lintowania lub łączenia kodu w czasie działania w przeglądarce.

Jak korzystać z zaufanych typów

Przygotowanie do zgłaszania naruszeń zasad Content Security Policy

Możesz wdrożyć kolektor raportów, taki jak reporting-api-processor lub go-csp-collector, albo użyć jednego z jego 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 dodając odbiornik:

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

Dodaj nagłówek 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 na przykład aplikacja przekazuje ciąg znaków do funkcji innerHTML, przeglądarka wysyła 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 funkcja innerHTML została wywołana 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ąć naruszający kod, skorzystać z biblioteki, utworzyć zasadę zaufanego typu lub 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 opakowany w obiekcie TrustedHTML, dzięki czemu przeglądarka nie wygeneruje naruszenia.

Tworzenie zasad dotyczących zaufanych typów

Czasami nie można usunąć kodu powodującego naruszenie. Nie ma też biblioteki, która mogłaby oczyścić wartość i utworzyć za Ciebie 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żyj zasady domyślnej

Czasami nie możesz zmienić kodu, który powoduje problemy, na przykład wtedy, 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 zbiorniku jest używany ciąg znaków, który akceptuje tylko zaufany typ.

Przełącz na egzekwowanie zasad 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