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.
- Manipulowanie skryptem:
<script src>
i ustawianie zawartości tekstowej elementów<script>
. - Generowanie kodu HTML z ciągu znaków:
- Wykonywanie treści w pluginie:
- Kompilacja kodu JavaScript w czasie wykonywania:
eval
setTimeout
setInterval
new Function()
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:
anElement.innerHTML = location.href;
Aby wskazać, że dane zostały przetworzone bezpiecznie, utwórz specjalny obiekt – zaufany typ.
anElement.innerHTML = aTrustedHTML;
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:
el.textContent = '';
const img = document.createElement('img');
img.src = 'xyz.jpg';
el.appendChild(img);
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, '<')
});
}
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; // '<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.