Skrypty krzyżowe (XSS), czyli możliwość wstrzykiwania złośliwych skryptów do aplikacji internetowej, od ponad dekady są jedną z największych luk w zabezpieczeniach internetowych.
Content Security Policy (CSP) to dodatkowa warstwa zabezpieczeń, która pomaga ograniczyć atak XSS. Aby skonfigurować CSP, dodaj do strony internetowej nagłówek HTTP Content-Security-Policy
i ustaw wartości, które kontrolują zasoby, które klient użytkownika może ładować dla tej strony.
Na tej stronie wyjaśniamy, jak używać CSP na podstawie liczb jednorazowych lub haszy do ograniczania XSS zamiast powszechnie używanych dostawców CSP opartych na liście dozwolonych hostów, które często opuszczają stronę pod kątem XSS, ponieważ w większości konfiguracji można je pominąć.
Kluczowy termin: jednorazowa liczba to liczba losowa używana tylko raz, której można użyć do oznaczenia tagu <script>
jako zaufanego.
Termin kluczowy: funkcja haszująca to funkcja matematyczna, która przekształca wartość wejściową w skompresowaną wartość liczbową nazywaną haszem. Możesz użyć skrótu (np. SHA-256), aby oznaczyć wbudowany tag <script>
jako zaufane.
Zasady bezpieczeństwa treści oparte na liczbach jednorazowych lub haszach są często nazywane rygorystycznymi CSP. Gdy aplikacja korzysta z rygorystycznego CSP, osoby przeprowadzające atak, które znajdą w niej błędy związane z wstrzykiwaniem kodu HTML, zazwyczaj nie mogą ich użyć, aby wymusić na przeglądarce wykonanie złośliwych skryptów w dokumencie z lukami. Wynika to z tego, że rygorystyczne zasady CSP zezwalają tylko na zahaszowane skrypty lub skrypty z poprawną wartością jednorazową wygenerowaną na serwerze. Osoby przeprowadzające atak nie mogą wykonać skryptu bez poznania prawidłowej liczby jednorazowej dla danej odpowiedzi.
Dlaczego warto korzystać z rygorystycznego CSP?
Jeśli Twoja witryna zawiera już CSP, który wygląda jak script-src www.googleapis.com
, prawdopodobnie nie będzie skuteczny w różnych witrynach. Ten typ CSP jest nazywany listą dozwolonych CSP. Wymagają wielu ulepszeń i mogą zostać ominięte przez osoby przeprowadzające atak.
Rygorystyczne CSP oparte na kryptograficznych liczbach jednorazowych lub haszach pozwalają uniknąć tych pułapek.
Rygorystyczna struktura CSP
Podstawowa rygorystyczna zasada bezpieczeństwa treści używa jednego z następujących nagłówków odpowiedzi HTTP:
Rygorystyczne CSP niewymagające korzystania z technologii
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
![](https://web.dev/static/articles/strict-csp/image/er4BaGCJzBwDaESFKfZd.jpg?authuser=7&hl=pl)
Ścisły CSP oparty na haszowaniu
Content-Security-Policy:
script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
Te właściwości sprawiają, że CSP jest taki jak ten „rygorystyczny” i dlatego bezpieczny:
- Wykorzystuje w nim liczby jednorazowe
'nonce-{RANDOM}'
lub hasze'sha256-{HASHED_INLINE_SCRIPT}'
, aby wskazać, które tagi<script>
powinny zostać uruchomione w przeglądarce użytkownika. - Ustawia ona
'strict-dynamic'
tak, aby ułatwić wdrażanie CSP opartego na liczbach jednorazowych lub haszach, ponieważ automatycznie zezwala na wykonywanie skryptów tworzonych przez zaufany skrypt. Odblokowuje to też korzystanie z większości zewnętrznych bibliotek i widżetów JavaScript. - Nie korzysta z list dozwolonych adresów URL, więc nie obawia się o popularne pomijanie CSP.
- Blokuje niezaufane skrypty wbudowane, takie jak moduły obsługi zdarzeń czy identyfikatory URI
javascript:
. - Uniemożliwia to
object-src
wyłączenie niebezpiecznych wtyczek, takich jak Flash. - Ogranicza ono
base-uri
do blokowania wstrzykiwania tagów<base>
. Dzięki temu hakerzy nie będą mogli zmienić lokalizacji skryptów wczytywanych ze względnych adresów URL.
Wdrożenie rygorystycznego CSP
Aby wdrożyć rygorystyczne zasady CSP:
- Zdecyduj, czy Twoja aplikacja powinna ustawić CSP w formacie liczby jednorazowej czy opartej na haszowaniu.
- Skopiuj CSP z sekcji Ścisła struktura CSP i ustaw go jako nagłówek odpowiedzi w aplikacji.
- refaktoryzuj szablony HTML i kod po stronie klienta, aby usunąć wzorce niezgodne z CSP;
- Wdróż CSP.
W ramach tego procesu możesz przeprowadzić audyt Lighthouse
(wersja 7.3.0 lub nowsza z flagą --preset=experimental
) Sprawdzonych metod
(w celu sprawdzenia, czy Twoja witryna spełnia wymagania dotyczące CSP i czy jest wystarczająco rygorystyczna).
![Raport Lighthouse z ostrzeżeniem, że nie znaleziono CSP w trybie egzekwowania.](https://web.dev/static/articles/strict-csp/image/lighthouse-report-warning-74f66e2ef76dc.png?authuser=7&hl=pl)
Krok 1. Zdecyduj, czy potrzebujesz CSP opartego na liczbach jednorazowych czy haszowych
Oto jak działają 2 typy rygorystycznego CSP:
CSP na podstawie wartości jednorazowej
W przypadku CSP opartej na liczbie jednorazowej generujesz losową liczbę w czasie działania, uwzględniasz ją w CSP i wiąże się z każdym tagiem skryptu na stronie. Osoba przeprowadzająca atak nie może umieścić ani uruchomić złośliwego skryptu na stronie, ponieważ musi odgadnąć, jaka liczba losowa jest dla niego losowa. Ta funkcja działa tylko wtedy, gdy liczba nie jest możliwa do odgadnięcia i jest generowane automatycznie w czasie działania dla każdej odpowiedzi.
W przypadku stron HTML renderowanych na serwerze używaj CSP oparty na liczbie jednorazowej. W przypadku takich stron można utworzyć nową losową liczbę dla każdej odpowiedzi.
CSP oparty na haszy
W przypadku CSP opartej na haszach hasz każdego wbudowanego tagu skryptu jest dodawany do CSP. Każdy skrypt ma inny hasz. Osoba przeprowadzająca atak nie może umieścić ani uruchomić złośliwego skryptu na stronie, ponieważ skrót tego skryptu musi znajdować się w CSP.
W przypadku stron HTML wyświetlanych statycznie lub stron, które mają być w pamięci podręcznej, używaj CSP opartych na haszach. Możesz na przykład użyć CSP opartego na haszach na potrzeby jednostronicowych aplikacji internetowych tworzonych za pomocą platform takich jak Angular, React i innych, które są wyświetlane statycznie bez renderowania po stronie serwera.
Krok 2. Ustaw rygorystyczny CSP i przygotuj skrypty
Podczas konfigurowania CSP masz kilka opcji:
- Tryb „tylko raporty” (
Content-Security-Policy-Report-Only
) lub tryb egzekwowania (Content-Security-Policy
). W trybie „tylko raporty” CSP nie blokuje jeszcze zasobów, więc nie ma przerw w działaniu witryny, ale możesz wyświetlać błędy i raportować wszystkie zablokowane elementy. Lokalnie, podczas konfigurowania CSP, nie ma to większego znaczenia, ponieważ oba tryby wyświetlają błędy w konsoli przeglądarki. Tryb egzekwowania może pomóc Ci znaleźć zasoby w wersji roboczej bloków CSP, ponieważ zablokowanie zasobu może sprawić, że strona będzie wyglądać nieprawidłowo. Tryb „Tylko raport” staje się najbardziej przydatny na późniejszym etapie procesu (patrz krok 5). - Nagłówek lub tag HTML
<meta>
. W przypadku programowania lokalnego tag<meta>
może być wygodniejszy w dostosowywaniu CSP i szybkim sprawdzaniu jego wpływu na Twoją witrynę. Pamiętaj jednak, że:- Później podczas wdrażania CSP w środowisku produkcyjnym zalecamy ustawienie go jako nagłówka HTTP.
- Jeśli chcesz ustawić CSP w trybie „tylko raport”, musisz ustawić go jako nagłówek, ponieważ metatagi CSP nie obsługują trybu „Tylko raport”.
Ustaw w swojej aplikacji ten nagłówek odpowiedzi HTTP Content-Security-Policy
:
Content-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none';
Generowanie liczby jednorazowej dla CSP
Liczba jednorazowa to losowa liczba używana tylko raz na wczytanie strony. CSP oparty na liczbie jednorazowej może ograniczać atak XSS tylko wtedy, gdy osoby przeprowadzające atak nie są w stanie odgadnąć wartości liczby jednorazowej. Jednorazowa wartość CSP musi mieć wartość:
- kryptograficznie silną wartość losową (najlepiej o długości ponad 128 bitów).
- Nowo generowane dla każdej odpowiedzi
- Z kodowaniem Base64
Oto kilka przykładów dodania jednorazowej liczby jednorazowej CSP w platformach po stronie serwera:
- Django (pyton)
- Express (JavaScript):
const app = express(); app.get('/', function(request, response) { // Generate a new random nonce value for every response. const nonce = crypto.randomBytes(16).toString("base64"); // Set the strict nonce-based CSP response header const csp = `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`; response.set("Content-Security-Policy", csp); // Every <script> tag in your application should set the `nonce` attribute to this value. response.render(template, { nonce: nonce }); });
Dodaj atrybut nonce
do elementów <script>
W przypadku CSP opartej na liczbie jednorazowej każdy element <script>
musi mieć atrybut nonce
, który pasuje do losowej wartości liczby jednorazowej określonej w nagłówku CSP. Wszystkie skrypty mogą mieć tę samą liczbę jednorazową. Pierwszym krokiem jest dodanie tych atrybutów do wszystkich skryptów, aby CSP na nie pozwalał.
Ustaw w swojej aplikacji ten nagłówek odpowiedzi HTTP Content-Security-Policy
:
Content-Security-Policy: script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic'; object-src 'none'; base-uri 'none';
W przypadku wielu wbudowanych skryptów składnia będzie taka: 'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'
.
Dynamiczne wczytywanie skryptów źródłowych
Skróty CSP są obsługiwane w przeglądarkach tylko w przypadku wbudowanych skryptów, dlatego wszystkie skrypty innych firm musisz wczytywać dynamicznie za pomocą wbudowanego skryptu. Skróty w przypadku skryptów źródłowych nie są dobrze obsługiwane w różnych przeglądarkach.
![](https://web.dev/static/articles/strict-csp/image/B2YsfJDYw8PRI6kJI7Bs.jpg?authuser=7&hl=pl)
<script> var scripts = [ 'https://example.org/foo.js', 'https://example.org/bar.js']; scripts.forEach(function(scriptUrl) { var s = document.createElement('script'); s.src = scriptUrl; s.async = false; // to preserve execution order document.head.appendChild(s); }); </script>Aby uruchomić ten skrypt, musisz obliczyć hasz skryptu wbudowanego i dodać go do nagłówka odpowiedzi CSP, zastępując zmienną
{HASHED_INLINE_SCRIPT}
. Aby zmniejszyć liczbę haszów, możesz scalić wszystkie wbudowane skrypty w jeden skrypt. Aby zobaczyć, jak to działa, zapoznaj się z tym przykładem i jego kodem.
<script src="https://example.org/foo.js"></script> <script src="https://example.org/bar.js"></script>CSP blokuje te skrypty, ponieważ szyfrujemy tylko skrypty wbudowane.
Uwagi na temat ładowania skryptu
Przykład wbudowanego skryptu dodaje element s.async = false
, aby zapewnić, że foo
zostanie wykonany przed bar
, nawet jeśli robot bar
wczyta się jako pierwszy. W tym fragmencie s.async = false
nie blokuje parsera podczas wczytywania skryptów, ponieważ skrypty są dodawane dynamicznie. Parser zatrzymuje się tylko na czas wykonywania skryptów, tak jak w przypadku skryptów async
. Pamiętaj jednak o tym fragmencie:
-
Jeden skrypt lub oba skrypty mogą zostać wykonane przed zakończeniem pobierania dokumentu. Jeśli chcesz, aby dokument był gotowy przed wykonaniem skryptów, poczekaj na zdarzenie
DOMContentLoaded
, zanim dodasz skrypty. Jeśli powoduje to problem z wydajnością, ponieważ pobieranie skryptów nie rozpoczyna się dostatecznie wcześnie, użyj wstępnego wczytywania tagów na stronie. -
defer = true
nic nie robi. W takim przypadku w razie potrzeby uruchom skrypt ręcznie.
Krok 3. refaktoryzuj szablony HTML i kod po stronie klienta
Do uruchamiania skryptów można używać wbudowanych modułów obsługi zdarzeń (np. onclick="…"
, onerror="…"
) i identyfikatorów URI JavaScript (<a href="javascript:…">
). Oznacza to, że osoba przeprowadzająca atak, która znajdzie błąd XSS, może wstrzyknąć taki kod HTML i wykonać złośliwy kod JavaScript. CSP oparty na liczbach jednorazowych lub haszach zakazuje używania tego rodzaju znaczników.
Jeśli Twoja witryna korzysta z któregokolwiek z tych wzorców, musisz przekształcić go w bezpieczniejsze alternatywy.
Jeśli w poprzednim kroku włączono CSP, naruszenia zasad CSP będą widoczne w konsoli za każdym razem, gdy CSP zablokuje niezgodny wzorzec.
![Raporty o naruszeniach zasad CSP w konsoli programisty Chrome.](https://web.dev/static/articles/strict-csp/image/csp-violation-reports-th-ff599b24b17be.jpg?authuser=7&hl=pl)
W większości przypadków rozwiązanie jest proste:
Refaktoryzacja wbudowanych modułów obsługi zdarzeń
<span id="things">A thing.</span> <script nonce="${nonce}"> document.getElementById('things').addEventListener('click', doThings); </script>CSP zezwala na moduły obsługi zdarzeń zarejestrowane za pomocą JavaScriptu.
<span onclick="doThings();">A thing.</span>CSP blokuje wbudowane moduły obsługi zdarzeń.
Refaktoryzuj identyfikatory URI javascript:
<a id="foo">foo</a> <script nonce="${nonce}"> document.getElementById('foo').addEventListener('click', linkClicked); </script>CSP zezwala na moduły obsługi zdarzeń zarejestrowane za pomocą JavaScriptu.
<a href="javascript:linkClicked()">foo</a>CSP blokuje identyfikatory URI obiektu javascript:.
Usuń eval()
z kodu JavaScript
Jeśli Twoja aplikacja używa metody eval()
do konwertowania serializacji ciągów znaków JSON na obiekty JS, należy dokonać refaktoryzacji takich instancji na JSON.parse()
, co również jest szybsze.
Jeśli nie możesz usunąć wszystkich przypadków użycia eval()
, nadal możesz ustawić rygorystyczny CSP oparty na liczbie jednorazowej, ale musisz użyć słowa kluczowego CSP 'unsafe-eval'
, co sprawia, że Twoja zasada jest nieco mniej bezpieczna.
Te i inne przykłady takiej refaktoryzacji znajdziesz w tym rygorystycznym ćwiczeniu w programowaniu CSP:
Krok 4 (opcjonalny). Dodaj wartości zastępcze w celu obsługi starszych wersji przeglądarek
Jeśli chcesz zapewnić obsługę starszych wersji przeglądarek:
- Użycie dyrektywy
strict-dynamic
wymaga dodania elementuhttps:
jako kreacji zastępczej dla wcześniejszych wersji Safari. Gdy to zrobisz:- Wszystkie przeglądarki obsługujące
strict-dynamic
ignorują wartość zastępcząhttps:
, więc nie zmniejszy to skuteczności zasady. - W starszych przeglądarkach skrypty zewnętrzne mogą wczytywać się tylko wtedy, gdy pochodzą ze źródła HTTPS. Jest to mniej bezpieczne niż rygorystyczny CSP, ale nadal zapobiega niektórym typowym przyczynom XSS, np. wstrzykiwaniu identyfikatorów URI
javascript:
.
- Wszystkie przeglądarki obsługujące
- Aby zapewnić zgodność z bardzo starymi wersjami przeglądarek (przez co najmniej 4 lata), możesz dodać
unsafe-inline
jako kreację zastępczą. Jeśli obecna jest liczba jednorazowa lub hasz CSP, wszystkie ostatnie przeglądarki ignorują parametrunsafe-inline
.
Content-Security-Policy:
script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none';
base-uri 'none';
Krok 5. Wdróż CSP
Gdy potwierdzisz, że dostawca CSP nie blokuje żadnych prawidłowych skryptów w lokalnym środowisku programistycznym, możesz wdrożyć go na etapie przejściowym, a potem w środowisku produkcyjnym:
- (Opcjonalnie) Wdróż CSP w trybie „tylko raporty”, korzystając z nagłówka
Content-Security-Policy-Report-Only
. Tryb „Tylko raportowanie” przydaje się do testowania potencjalnie naruszających zasady zmian, takich jak nowy dostawca CSP w środowisku produkcyjnym, zanim zaczniesz egzekwować ograniczenia CSP. W trybie „tylko do raportów” CSP nie wpływa na działanie aplikacji, ale w przypadku napotkania wzorców niezgodnych z tym CSP przeglądarka generuje błędy w konsoli i raporty o naruszeniach. Dzięki temu możesz zobaczyć, co nie działa u użytkowników. Więcej informacji znajdziesz w artykule Interfejs API do raportowania. - Gdy masz pewność, że dostawca CSP nie zapewni użytkownikom dostępu do Twojej witryny, wdróż go za pomocą nagłówka odpowiedzi
Content-Security-Policy
. Zalecamy ustawianie CSP za pomocą nagłówka HTTP po stronie serwera, ponieważ jest to bezpieczniejsze niż tag<meta>
. Gdy to zrobisz, CSP zacznie chronić Twoją aplikację przed XSS.
Ograniczenia
Rygorystyczne CSP zwykle zapewnia silną dodatkową warstwę zabezpieczeń, która pomaga ograniczyć ryzyko XSS. W większości przypadków CSP znacznie zmniejsza powierzchnię ataku, odrzucając niebezpieczne wzorce, takie jak identyfikatory URI javascript:
. Jednak w zależności od rodzaju używanego standardu CSP (noce, hasze, z elementem 'strict-dynamic'
lub bez niego) w niektórych przypadkach CSP również nie chroni Twojej aplikacji:
- Jeśli wykonasz kod jednorazowy, ale wstrzykniesz ją bezpośrednio w treści lub w parametrze
src
tego elementu<script>
. - jeśli występują wstrzykiwanie w lokalizacji skryptów tworzonych dynamicznie (
document.createElement('script')
), w tym do funkcji biblioteki, które tworzą węzły DOMscript
na podstawie wartości ich argumentów. Obejmuje to niektóre typowe interfejsy API, takie jak.html()
jQuery, a także.get()
i.post()
w jQuery w wersji < 3.0. - Jeśli do starszych aplikacji AngularJS są stosowane wstrzykiwanie szablonów. Osoba przeprowadzająca atak, która może wstawić go do szablonu AngularJS, może go użyć do wykonania dowolnego JavaScriptu.
- Jeśli zasada zawiera
'unsafe-eval'
, wstrzykiwanie doeval()
,setTimeout()
i kilku innych rzadko używanych interfejsów API.
Deweloperzy i inżynierowie zabezpieczeń powinni zwrócić szczególną uwagę na takie wzorce podczas weryfikacji kodu i kontroli zabezpieczeń. Więcej informacji na temat takich przypadków znajdziesz w artykule Content Security Policy: A sukces między wzmocnieniem a ograniczeniem.
Więcej informacji
- CSP nie żyje, niech żyje CSP! W kwestii bezpieczeństwa białych list i przyszłości polityki bezpieczeństwa treści
- Oceniający CSP
- Konferencja w LocoMoco: Content Security Policy – skuteczny bałagan między wzmocnieniem a ograniczeniem
- Prezentacja Google I/O: zabezpieczanie aplikacji internetowych za pomocą nowoczesnych funkcji platformy