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

Lukas Weichselbaum
Lukas Weichselbaum

Obsługa przeglądarek

  • Chrome: 52.
  • Edge: 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 używać CSP opartego na wartościach nonce lub haszach, aby ograniczyć ryzyko ataków XSS, zamiast powszechnie stosowanych CSP opartych na liście dozwolonych hostów, które często pozostawiają stronę bezbronną przed atakami XSS, ponieważ można je ominąć w większości konfiguracji.

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 wstrzykiwaniu kodu HTML, nie mogą ich wykorzystać do wymuszenia na przeglądarce wykonania złośliwych skryptów w dokumentach z luką. Wynika to z tego, że ścisłe zasady CSP zezwalają tylko na szyfrowane skrypty lub skrypty z prawidłową wartością nonce wygenerowaną na serwerze. Dzięki temu atakujący nie mogą uruchomić skryptu bez znajomości prawidłowej wartości nonce dla danej odpowiedzi.

Dlaczego warto używać rygorystycznego CSP?

Jeśli w Twojej witrynie jest już tag CSP podobny do script-src www.googleapis.com, prawdopodobnie nie jest on skuteczny w przeciwdziałaniu atakom między witrynami. Ten typ CSP nazywamy listą dozwolonych CSP. Wymagają one wielu dostosowań i mogą być omijane przez atakujących.

W przypadku rygorystycznych zasad CSP opartych na kryptograficznych identyfikatorach losowych lub wartościach skrótowych nie ma takich problemów.

Struktura ścisła 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';

Właściwości, które sprawiają, że usługa CSP jest „rygorystyczna”, a co za tym idzie bezpieczna:

  • 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 używanie większości zewnętrznych bibliotek JavaScriptu i widżetów.
  • Nie opiera się na listach dozwolonych adresów URL, więc nie jest podatny na częste obejścia CSP.
  • Blokuje niesprawdzone skrypty wstawiane inline, takie jak moduły obsługi zdarzeń wstawiane inline czy identyfikatory URI danych.javascript:
  • Ogranicza ono object-src do wyłączania niebezpiecznych wtyczek, takich jak Flash.
  • Ogranicza ona base-uri, aby zablokować wstrzykiwanie tagów <base>. 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 nagłówek CSP, musisz:

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

W trakcie tego procesu możesz użyć Lighthouse (w wersji 7.3.0 lub nowszej z flagą --preset=experimental) do sprawdzenia, czy Twoja witryna ma zasadę CSP i czy jest ona wystarczająco rygorystyczna, aby skutecznie chronić przed atakami XSS.

Ostrzeżenie w raporcie Lighthouse dotyczące braku CSP w trybie egzekwowania.
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 rygorystycznych zasad CSP:

CSP oparty na niepowtarzalnych identyfikatorach

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ą losową liczbę 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 łańcuchu bloków

W przypadku CSP opartego na łańcuchu haszowym do CSP jest dodawany łańcuch haszowy każdego tagu skryptu wbudowanego. 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ż aby mógł on działać, jego hasz musi znajdować się 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ą dostarczane 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. Tryb egzekwowania może pomóc w znalezieniu zasobów blokowanych przez wersję roboczą CSP, ponieważ zablokowanie zasobu może spowodować, że strona będzie wyglądać na uszkodzoną. Tryb tylko do raportowania jest najbardziej przydatny w późniejszej fazie procesu (patrz Krok 5).
  • nagłówku lub tagu HTML <meta>. W przypadku rozwoju lokalnego tag <meta> może być wygodniejszy do dostosowania CSP i szybkiego sprawdzenia, jaki ma on wpływ na witrynę. Jednak:
    • Podczas późniejszego wdrażania CSP w produkcji 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

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

Niepowtarzalny identyfikator to losowa liczba używana tylko raz podczas wczytywania strony. CSP oparty na szyfrowaniu symetrycznym może ograniczyć XSS tylko wtedy, gdy atakujący nie może odgadnąć wartości szyfrowania symetrycznego. CSP nonce musi:

  • 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 nonce’a CSP w ramkach 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 });
});

dodać 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 hasz i dodać go do nagłówka odpowiedzi CSP, zastępując tym samym miejsce zarezerwowane dla {HASHED_INLINE_SCRIPT}. Aby zmniejszyć liczbę haszy, możesz scalić wszystkie skrypty wbudowane 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, który pasuje do dozwolonego źródła.

Ważne informacje o wczytywaniu skryptów

Przykład skryptu w ciele wiadomości dodaje tag 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 funkcja s.async = false nie blokuje parsowania podczas wczytywania skryptów, ponieważ skrypty są dodawane dynamicznie. Parser przestaje działać 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ć wykonane, 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 nie robi nic. Jeśli potrzebujesz takiego zachowania, uruchom skrypt ręcznie, gdy zajdzie taka potrzeba.

Krok 3. Przerzuć szablony HTML i kod 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 używa któregoś z tych wzorów, musisz go przerobić na bezpieczniejszą alternatywę.

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ń wbudowanych.

Przerzuć javascript: identyfikatory URI

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 adresy internetowe JavaScript: URI.

Usuń eval() z kodu JavaScript

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 możesz usunąć wszystkich wystąpień eval(), nadal możesz skonfigurować ścisłą politykę CSP o zasadzie nonce, ale musisz użyć słowa kluczowego CSP 'unsafe-eval', co spowoduje, że polityka będzie 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.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

Źródło

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

  • Korzystanie z funkcji strict-dynamic wymaga dodania opcji https: jako opcji zapasowej w przypadku starszych wersji przeglądarki 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 starszych przeglądarkach skrypty pochodzące z zewnątrz mogą się wczytywać tylko wtedy, gdy pochodzą z ź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 starszymi wersjami przeglądarek (starszymi niż 4 lata), możesz dodać opcję unsafe-inline jako alternatywę. 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 istotnych zmian, takich 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 się stanie z użytkownikami. Więcej informacji znajdziesz w artykule Interfejs Reporting API.
  2. Gdy będziesz mieć 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 zapobiegać atakom 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 opcją 'strict-dynamic' lub bez niej), 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 o tych przypadkach znajdziesz w artykule Zasady bezpieczeństwa treści: skuteczne zabezpieczenie i ograniczenie zagrożeń.

Więcej informacji