Ogranicz ataki typu cross-site scripting (XSS) przy użyciu rygorystycznej zasady Content Security Policy (CSP)

Obsługa przeglądarek

  • Chrome: 52.
  • Krawędź: 79.
  • Firefox: 52.
  • Safari: 15.4.

Źródło

Cross-site scripting (XSS), czyli możliwość wstrzyknięcia szkodliwych skryptów do aplikacji internetowej, od ponad dekady jest jedną z największych luk w zabezpieczeniach sieci.

Content Security Policy (CSP) to dodatkowa warstwa zabezpieczeń, która pomaga ograniczać ataki typu XSS. Aby skonfigurować CSP, dodaj nagłówek HTTP Content-Security-Policy do strony internetowej i ustaw wartości, które określają, jakie zasoby może wczytywać klient użytkownika na tej stronie.

Na tej stronie wyjaśniamy, jak zamiast powszechnie stosowanych CSP z użyciem listy dozwolonych hostów, które często pozostawiają stronę podatną na ataki XSS, ponieważ można je ominąć w większości konfiguracji, używać CSP opartego na wartościach nonce lub haszach, aby ograniczyć ryzyko ataków XSS.

Termin kluczowy: nonce to losowy numer używany tylko raz, który możesz wykorzystać do oznaczenia tagu <script> jako zaufany.

Termin kluczowy: funkcja szyfrująca to funkcja matematyczna, która zamienia wartość wejściową na skompresowaną wartość liczbową zwaną szyfrem. Aby oznaczyć tag <script> jako zaufany, możesz użyć skrótu (np. SHA-256).

Zasady Content Security Policy oparte na wartościach nonce lub haszach są często nazywane surowymi zasadami CSP. Jeśli aplikacja używa rygorystycznych zasad CSP, hakerzy, którzy znajdą luki w zabezpieczeniach umożliwiające wstrzykiwanie kodu HTML, nie mogą ich wykorzystać do wymuszenia na przeglądarce wykonania złośliwych skryptów w dokumentach zawierających luki. Wynika to z faktu, że rygorystyczne zasady CSP dopuszczają tylko zaszyfrowane skrypty lub skrypty z poprawną wartością jednorazową wygenerowaną na serwerze, więc osoby przeprowadzające atak nie mogą wykonać skryptu bez znajomości poprawnej wartości jednorazowej danej odpowiedzi.

Dlaczego warto używać rygorystycznego CSP?

Jeśli w Twojej witrynie jest już standard CSP script-src www.googleapis.com, prawdopodobnie nie jest on skuteczny w przypadku innych witryn. Ten typ CSP nazywamy listą dozwolonych CSP. Wymagają one wielu dostosowań i mogą być omijane przez atakujących.

Ścisłe zasady CSP oparte na kryptograficznych identyfikatorach losowych lub haszach unikają tych pułapek.

Ścisła struktura CSP

Podstawowa rygorystyczna zasada Content Security Policy używa jednego z tych nagłówków odpowiedzi HTTP:

Sztywne reguły CSP oparte na szyfrze nonce

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
Jak działa rygorystyczny CSP oparty na szyfrach jednorazowych

Szczegółowe zasady CSP oparte na haśle

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, taki jak ten, jest „ścisły”, a tym samym bezpieczny:

  • Używa ona losowych liczb 'nonce-{RANDOM}' lub haszy 'sha256-{HASHED_INLINE_SCRIPT}', aby wskazać, które tagi <script> są uruchamiane w przeglądarce użytkownika przez zaufanego dewelopera witryny.
  • Ustawia ona wartość 'strict-dynamic', aby zmniejszyć nakład pracy związany z wdrażaniem CSP na podstawie liczby jednorazowej lub hasza, automatycznie zezwalając na wykonywanie skryptów utworzonych przez zaufany skrypt. Pozwala to też na korzystanie z większości zewnętrznych bibliotek i widżetów JavaScriptu.
  • Nie opiera się na listach dozwolonych adresów URL, więc nie jest podatna na częste obejścia CSP.
  • Blokuje niesprawdzone skrypty wstawiane inline, takie jak moduły obsługi zdarzeń wstawiane inline czy identyfikatory URI danych.
  • Ogranicza ona object-src do wyłączania niebezpiecznych wtyczek, takich jak Flash.
  • Ogranicza działanie usługi base-uri do blokowania wstrzykiwania <base> tagów. Zapobiega to zmianie przez atakujących lokalizacji skryptów wczytywanych z względnych adresów URL.

Stosowanie ścisłych zasad CSP

Aby wdrożyć rygorystyczny CSP, musisz:

  1. Zdecyduj, czy aplikacja ma ustawiać CSP na podstawie liczby jednorazowej czy hasza.
  2. Skopiuj CSP z sekcji Rygorystyczna struktura CSP i ustaw go jako nagłówek odpowiedzi w aplikacji.
  3. Zrefaktoryzuj szablony HTML i kod po stronie klienta, aby usunąć wzorce niezgodne z CSP.
  4. Wdróż CSP.

W ramach tego procesu możesz użyć Lighthouse (wersja 7.3.0 lub nowsza z flagą --preset=experimental), Sprawdzone metody podczas tego procesu, aby sprawdzić, czy witryna ma CSP i czy jest ona wystarczająco rygorystyczna, aby była skuteczna w odniesieniu do XSS.

Raport Lighthouse z ostrzeżeniem, że w trybie egzekwowania nie znaleziono CSP.
Jeśli w witrynie nie ma zasad CSP, Lighthouse wyświetla to ostrzeżenie.

Krok 1. Zdecyduj, czy potrzebujesz CSP opartego na liczbie losowym lub na haszu

Oto jak działają 2 rodzaje ścisłych zasad CSP:

CSP oparty na niepowtarzalnych identyfikatorach (nonce)

W przypadku CSP opartego na nonce generujesz losową liczbę w czasie wykonywania, umieszczasz ją w swojej polityce CSP i kojarzysz z każdym tagiem skryptu na stronie. Atakujący nie może umieścić na Twojej stronie ani uruchomić złośliwego skryptu, ponieważ musiałby odgadnąć prawidłową liczbę losową dla tego skryptu. Ta metoda działa tylko wtedy, gdy numer nie jest łatwy do odgadnięcia i jest generowany na nowo w czasie wykonywania dla każdej odpowiedzi.

Używaj CSP opartego na nonce w przypadku stron HTML renderowanych na serwerze. W przypadku tych stron możesz utworzyć nową losową liczbę dla każdej odpowiedzi.

Standard CSP oparty na haśle

W przypadku CSP opartych na haszach do CSP jest dodawany hasz każdego wbudowanego tagu skryptu. Każdy skrypt ma inny ciąg znaków. Atakujący nie może umieścić na Twojej stronie ani uruchomić złośliwego skryptu, ponieważ do jego uruchomienia potrzebny jest hasz skryptu w Twoim CSP.

Używaj CSP opartego na haśle w przypadku stron HTML wyświetlanych statycznie lub stron, które muszą być przechowywane w pamięci podręcznej. Możesz na przykład używać CSP opartego na haśle w przypadku jednostronicowych aplikacji internetowych utworzonych za pomocą frameworków takich jak Angular, React lub innych, które są wyświetlane statycznie bez renderowania po stronie serwera.

Krok 2. Skonfiguruj ścisłe zasady CSP i przygotuj skrypty

Podczas konfigurowania CSP masz do wyboru kilka opcji:

  • Tryb tylko raportów (Content-Security-Policy-Report-Only) lub tryb egzekwowania (Content-Security-Policy). W trybie tylko raportów CSP nie blokuje jeszcze zasobów, więc nic w Twojej witrynie się nie zepsuje, ale możesz zobaczyć błędy i otrzymać raporty dotyczące wszystkiego, co zostałoby zablokowane. Podczas konfigurowania CSP lokalnie nie ma to większego znaczenia, ponieważ oba tryby wyświetlają błędy w konsoli przeglądarki. W razie potrzeby tryb egzekwowania pomoże Ci znaleźć zasoby blokowane przez CSP w wersji roboczej, ponieważ zablokowanie zasobu może sprawić, że strona będzie wyglądała na uszkodzone. Tryb tylko do raportowania jest najbardziej przydatny w późniejszej fazie procesu (patrz Krok 5).
  • nagłówku lub tagu HTML <meta>. W przypadku programowania lokalnego tag <meta> może być wygodniejszy do dostosowywania CSP i szybkiego sprawdzania, jak wpływa on na Twoją witrynę. Pamiętaj jednak, że:
    • Później przy wdrażaniu CSP w środowisku produkcyjnym zalecamy ustawienie go jako nagłówka HTTP.
    • Jeśli chcesz ustawić CSP w trybie tylko do raportowania, musisz go ustawić jako nagłówek, ponieważ metatagi CSP nie obsługują trybu tylko do raportowania.

Opcja A. Standard CSP oparty na niepowtarzalnych identyfikatorach (nonce)

W aplikacji ustaw ten nagłówek odpowiedzi HTTP Content-Security-Policy:

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Generowanie nonce’a dla CSP

Liczba jednorazowa to liczba losowa używana tylko raz na wczytanie strony. CSP oparty na wartościach nonce może ograniczyć XSS tylko wtedy, gdy atakujący nie może odgadnąć wartości nonce. Liczba jednorazowa CSP musi być:

  • wartość losowa o wysokiej odporności na szyfrowanie (najlepiej o długości co najmniej 128 bitów);
  • nowo generowane w przypadku każdej odpowiedzi;
  • zakodowany w formacie Base64,

Oto kilka przykładów dodawania liczby jednorazowej CSP w ramach platform po stronie serwera:

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 opartego na szyfrowaniu symetrycznym każdy element <script> musi mieć atrybut nonce, który odpowiada losowej wartości nonce określonej w nagłówku CSP. Wszystkie skrypty mogą mieć ten sam nonce. Najpierw dodaj te atrybuty do wszystkich skryptów, aby CSP zezwolił na ich używanie.

Opcja B. Nagłówek odpowiedzi CSP oparty na haśle

W aplikacji ustaw 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 skryptów w ciele wiadomości składnia wygląda tak:'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'.

Dynamiczne wczytywanie skryptów źródłowych

Skrypty innych firm możesz wczytywać dynamicznie za pomocą skryptu wbudowanego.

Przykład wstawiania skryptów w tekście.
Dozwolone przez CSP
<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 umożliwić uruchomienie tego skryptu, musisz obliczyć jego ciąg znaków i dodać go do nagłówka odpowiedzi CSP, zastępując tym samym placeholder {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.
Zablokowane przez CSP
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
CSP blokuje te skrypty, ponieważ nie zostały one dodane dynamicznie i nie mają atrybutu integrity pasującego do dozwolonego źródła.

Uwagi na temat wczytywania skryptu

Przykład skryptu w ciele wiadomości dodaje s.async = false, aby zapewnić, że skrypt foo zostanie wykonany przed skryptem bar, nawet jeśli skrypt bar zostanie załadowany jako pierwszy. W tym fragmencie kodu s.async = false nie blokuje parsera podczas wczytywania skryptów, ponieważ skrypty są dodawane dynamicznie. Parser zatrzymuje się tylko podczas wykonywania skryptów, tak jak w przypadku skryptów async. Pamiętaj jednak, że w przypadku tego fragmentu kodu:

  • Jeden lub oba skrypty mogą zostać uruchomione, zanim dokument zostanie pobrany. Jeśli chcesz, aby dokument był gotowy do momentu wykonania skryptów, zaczekaj na zdarzenie DOMContentLoaded, zanim dołączysz skrypty. Jeśli powoduje to problemy z wydajnością, ponieważ skrypty nie zaczynają się wczytywać odpowiednio wcześnie, użyj tagów wstępnego wczytania wcześniej na stronie.
  • defer = true nic nie robi. W razie potrzeby uruchom skrypt ręcznie.

Krok 3. Refaktoryzacja szablonów HTML i kodu po stronie klienta

Do uruchamiania skryptów można używać wbudowanych obciążników zdarzeń (takich jak onclick="…" czy onerror="…") oraz adresów URI kodu JavaScript (<a href="javascript:…">). Oznacza to, że atakujący, który znajdzie błąd XSS, może wstrzyknąć ten rodzaj kodu HTML i wykonać złośliwy kod JavaScript. Zasady CSP oparte na liczbie losowym lub haśle zabraniają używania tego rodzaju znaczników. Jeśli Twoja witryna korzysta z któregoś z tych wzorców, musisz zmienić je na bezpieczniejsze alternatywy.

Jeśli w poprzednim kroku włączysz CSP, będziesz widzieć naruszenia zasad CSP w konsoli za każdym razem, gdy CSP zablokuje niezgodny wzór.

raporty o naruszeniu CSP w konsoli programisty Chrome.
Błędy konsoli dotyczące zablokowanego kodu.

W większości przypadków rozwiązanie jest proste:

Refaktoryzacja wbudowanych modułów obsługi zdarzeń

Dozwolone przez CSP
<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.
Zablokowane przez CSP
<span onclick="doThings();">A thing.</span>
CSP blokuje moduły obsługi zdarzeń w źródle.

Refaktoryzacja identyfikatorów URI javascript:

Dozwolone przez CSP
<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.
Zablokowane przez CSP
<a href="javascript:linkClicked()">foo</a>
CSP blokuje identyfikatory URI w języku JavaScript:

Usuń eval() z JavaScriptu

Jeśli Twoja aplikacja używa funkcji eval() do konwertowania serializacji ciągu znaków JSON na obiekty JS, powinna przerobić takie instancje na funkcję JSON.parse(), która jest też szybsza.

Jeśli nie da się usunąć wszystkich zastosowań eval(), nadal możesz ustawić rygorystyczne CSP na podstawie liczby jednorazowej, ale musisz użyć słowa kluczowego CSP 'unsafe-eval', przez co zasada jest nieco mniej bezpieczna.

Te i inne przykłady refaktoryzacji znajdziesz w tym ćwiczeniu z programowania dotyczącym ścisłego CSP:

Krok 4 (opcjonalny). Dodaj alternatywne rozwiązania, aby obsługiwać starsze wersje przeglądarek

Obsługa przeglądarek

  • Chrome: 52.
  • Krawędź: 79.
  • Firefox: 52.
  • Safari: 15.4.

Źródło

Jeśli musisz obsługiwać starsze wersje przeglądarek:

  • Użycie elementu strict-dynamic wymaga dodania elementu https: jako kreacji zastępczej we wcześniejszych wersjach Safari. Gdy to zrobisz:
    • Wszystkie przeglądarki, które obsługują strict-dynamic, ignorują wartość zastępczą https:, więc nie osłabi to skuteczności zasad.
    • W starych przeglądarkach skrypty pozyskane z zewnątrz mogą być wczytywane tylko wtedy, gdy pochodzą ze źródła HTTPS. Jest to mniej bezpieczne niż ścisłe reguły CSP, ale nadal zapobiega niektórym typowym przyczynom XSS, takim jak wstrzykiwanie identyfikatorów URI javascript:.
  • Aby zapewnić zgodność ze bardzo starszymi wersjami przeglądarek (ponad 4 lata), możesz dodać unsafe-inline jako opcję zastępczą. Wszystkie nowe przeglądarki ignorują unsafe-inline, jeśli jest obecny nonce lub hasz CSP.
Content-Security-Policy:
  script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';

Krok 5. Wdróż usługę CSP

Po potwierdzeniu, że CSP nie blokuje żadnych prawidłowych skryptów w lokalnym środowisku programistycznym, możesz wdrożyć CSP w środowisku testowym, a potem w środowisku produkcyjnym:

  1. (Opcjonalnie) Wdróż usługę CSP w trybie tylko do raportowania, używając nagłówka Content-Security-Policy-Report-Only. Tryb tylko do raportowania jest przydatny do testowania potencjalnie nieprawidłowej zmiany, takiej jak nowy CSP w środowisku produkcyjnym, zanim zaczniesz stosować ograniczenia CSP. W trybie tylko raportów nagłówek CSP nie wpływa na działanie aplikacji, ale przeglądarka nadal generuje błędy konsoli i raporty o naruszeniu, gdy wykryje wzorce niezgodne z tym nagłówkiem. Dzięki temu możesz sprawdzić, co nie działałoby u użytkowników. Więcej informacji znajdziesz w artykule Reporting API.
  2. Gdy masz pewność, że zasada CSP nie spowoduje problemów w witrynie dla użytkowników końcowych, wdrożenie zasad CSP za pomocą nagłówka odpowiedzi Content-Security-Policy. Zalecamy skonfigurowanie CSP za pomocą nagłówka HTTP po stronie serwera, ponieważ jest on bezpieczniejszy niż tag <meta>. Po wykonaniu tego kroku usługa CSP zacznie chronić Twoją aplikację przed atakami XSS.

Ograniczenia

Ścisłe przestrzeganie standardu CSP zapewnia dodatkową warstwę zabezpieczeń, która pomaga ograniczać ataki typu XSS. W większości przypadków CSP znacznie zmniejsza obszar ataku, odrzucając niebezpieczne wzorce, takie jak adresy URI javascript:. Jednak w zależności od typu CSP, którego używasz (liczby jednorazowe, hasze, z 'strict-dynamic' lub bez), mogą wystąpić przypadki, w których CSP nie chroni Twojej aplikacji:

  • Jeśli skrypt jest zabezpieczony, ale wstrzyknięcie występuje bezpośrednio w treści lub parametrze src elementu <script>.
  • Jeśli występują wstrzyknięcia w miejscach skryptów tworzonych dynamicznie (document.createElement('script')), w tym w dowolnych funkcjach biblioteki, które tworzą węzły modelu DOM script na podstawie wartości swoich argumentów. Obejmuje to niektóre popularne interfejsy API, takie jak .html() w jQuery, a także .get() i .post() w jQuery < 3.0.
  • Jeśli w starych aplikacjach AngularJS występują wstrzyknięcia szablonów. Osoba atakująca, która może wstrzyknąć kod do szablonu AngularJS, może go użyć do wykonania dowolnego kodu JavaScript.
  • Jeśli polityka zawiera 'unsafe-eval', wstrzyknięcia do eval(), setTimeout() i kilka innych rzadko używanych interfejsów API.

Programiści i specjaliści ds. bezpieczeństwa powinni zwracać szczególną uwagę na takie wzorce podczas przeglądania kodu i audytów bezpieczeństwa. Więcej informacji na temat takich przypadków znajdziesz w artykule Content Security Policy: A Successful Mess Between Hardening and Łatigation (Polityka zabezpieczeń treści: udana korespondencja między wzmocnieniem a ograniczeniem).

Więcej informacji