Content Security Policy – poziomy dojrzałości

Content Security Policy – poziomy dojrzałości

Tworzenie bezpiecznych aplikacji internetowych nie jest proste. Bardzo łatwo jest zostawić w nich podatności, narażając użytkowników na różne zagrożenia. Można by tu wymieniać dziesiątki z nich, ale skupmy się tutaj na kilku poniższych:

  • Clickjacking – wyświetlenie użytkownikowi zachęty do kliknięcia, wskutek którego wykona on niechcianą akcję w innej aplikacji, 
  • ManInMiddle – możliwość podsłuchania i modyfikacji requestów aplikacji do serwera, 
  • Formjacking – przejęcie danych wprowadzanych przez użytkownika w formularzach, 
  • Unvalidated Redirects – niezaplanowane przez twórców przekierowania na zewnętrzne adresy, XSS – wykonanie złośliwego kodu na przegladarce innego użytkownika. 

To tylko garść z możliwych zagrożeń, które mogą czekać na naszą aplikację. Wybrałem je celowo, ponieważ przed tymi wszystkimi zagrożeniami jesteśmy w stanie zabezpieczyć się, korzystając z jednego tylko mechanizmu – Content-Security-Policy (CSP). Co ciekawe większość współczesnych przeglądarek już wspiera to zabezpieczenie.

Content Security Policy - wsparcie
Content Security Policy – wsparcie

Oczywiście w niektórych przypadkach użycie CSP nie jest pełnym zabezpieczeniem, jednak mechanizm ten zmniejsza przestrzeń potencjalnego ataku, co również jest wartościowe.

Działanie CSP

Zacznijmy od przedstawienia, czym jest Content-Security-Policy i w jaki sposób działa.

Content-Security-Policy jest ustawieniem strony internetowej, które definiuje dozwolone operacje jakie ta strona może wykonywać między innymi w ramach wykonywania kodu i pobierania go z innych źródeł. Później opiszę więcej możliwości.
W skrócie, dzięki tej opcji konfiguracyjnej możemy jasno zdefiniować, które domeny są bezpieczne (lub raczej znane) dla naszej aplikacji. W związku z tym będzie mogła ona odwoływać się tylko do tych domen podczas ładowania skryptów, wyświetlania obrazów lub wysyłania danych w requestach HTTP.

Przykład zachowania CSP widać na stronie: 
https://suvroc.github.io/security-demos/XSS/CSP-example.html

Ma ona zdefiniowane proste zasady dostępu:
Content-Security-Policy: default-src 'self'
oznacza to, że strona może mieć dostęp tylko do skryptów ze swojej domeny.

Dokładnie w ten sposób to działa. Jeżeli zerkniesz do kodu i do konsoli Chrome Developer Tools to zobaczysz, że pliki pobierane z tego samego serwera ładują się bez problemów.
<script src="../libs/jquery-3.6.0.min.js"></script>
<link href="../libs/bootstrap.min.css" rel="stylesheet">

Jednocześnie pobieranie plików z innej domeny jest  zablokowane.
<script src="https://example.com/file.js"></script><link href="https://example.com/file.css" rel="stylesheet">

Widać to w konsoli.

Błędy CSP
Błędy CSP

Konfiguracja CSP w aplikacji

Konfigurację Content-Security-Policy można dodać do swojej aplikacji na dwa niżej opisane sposoby.

Za pomocą headerów.

Jest to zalecana opcja, ponieważ wspiera więcej sytuacji. Możemy to ustawić na dowolnym zasobie sieciowym, który będzie wyświetlany przez przeglądarkę. 

Po prostu definiujemy odpowiedni nagłówek w response wiadomości.
Content-Security-Policy "default-src 'self';"

Ustawienie tej konfiguracji za pomocą headerów umożliwia też zdefiniowanie tego na poziomie całej aplikacji zamiast pojedynczych stron.

Warto wspomnieć również o dodatkowym, pomocniczym nagłówku: 
Content-Security-Policy-Report-Only "default-src 'self';report-uri /some-report-uri;"

Dzięki niemu możemy uruchomić reguły CSP w trybie raportowania, a nie blokowania zabronionej treści. Ma to znaczenie, gdy dodajemy CSP do istniejącego projektu i nie chcemy zepsuć jego funkcjonalności. Możemy dzięki temu najpierw zdefiniować reguły, obserwować ich skutek w systemie, a dopiero potem włączyć, aby zaczęły obowiązywać.

Za pomocą meta tagu

Jest to uproszczona opcja, która pozwala w łatwy sposób dodać CSP do pojedynczych stron HTML. Dobrze się sprawdza w przypadku serwowania statycznych stron.

Rozwiązanie to ma swoje ograniczenie – w ten sposób nie można użyć niektórych opcji (frame-ancestors, report-uri, i sandbox).

Zawartość konfiguracji

Konfiguracja, czyli na przykład wartość nagłówka zawsze składa się z podobnych części:
Content-Security-Policy: <zakres interakcji> <dozwolone wartości>;<zakres interakcji> <dozwolone wartości>;

Zakres interakcji wskazuje nam na rodzaj działania, które konfigurujemy:

  • script-src – pobieranie skryptów, 
  • style-src – pobieranie stylu, 
  • frame-src – dołączanie <iframe>,
  • img-src – pobranie obrazów. 

Pełny spis wszystkich zakresów interakcji znajdziesz w dokumentacji MDN.

Wartość zaś wskazuje dozwolone źródła danej interakcji:

  • none – zabronione działania w danych typie interakcji, 
  • self – tylko z bieżącej domeny, 
  • unsafe-inline – bezpośrednio w kodzie, 
  • <adres> – zewnętrzne źródło. 

Analogicznie, pełny opis możesz znaleźć w odpowiedniej dokumentacji MDN

Poziomy dojrzałości konfiguracji Content-Security-Policy

CSP posiada wiele opcji. Można wyróżnić kilka poziomów dojrzałości konfiguracji tego zabezpieczenia.

Content-Security-Policy - poziomy dojrzałości
Content-Security-Policy – poziomy dojrzałości

Poziom 1 – tylko reguły default-src

Reguły default-src działają jak zbiór zasad propagowanych na wszystkie inne typy interakcji. Jeżeli zdefiniujemy wartość jako:

default-src 'self' cdn.example.com;

to znaczy, że zarówno skrypty JS jak i style oraz obrazki mogą być załadowane zarówno z bieżącej domeny jak i z domeny cdn.example.com.

Poziom 2 – reguły dla wielu typów interakcji

Różne typy interakcji istnieją po to, aby móc precyzyjniej określać dozwolone zachowania. Kosztuje to więcej pracy, ale w ten sposób możemy bardzo dokładnie określić, co jest dozwolone w ramach naszej aplikacji. Dzięki temu żadna interakcja, niezgodna z działaniem aplikacji, nie będzie mogła się wykonać. To skutecznie zabezpiecza przed uruchomieniem złośliwego kodu (XSS).

Dla przykładu poniższa konfiguracja jasno definiuje z jakich domen możemy pobierać skrypty i obrazy.
script-src 'self' cdn.example.com; img-src 'self' img.example.com;

Warto tu podkreślić, że opcja default-src jest aplikowalna tylko do interakcji, dla których nie zdefiniowaliśmy specyficznego ustawienia. Dla przykładu poniższa dyrektywa mówi, że obrazy mogą być ładowane tylko z domeny img.example.com. Nie mogą być pobierane z domeny aplikacji mimo zdefiniowania tego w default-src.

default-src 'self'; img-src img.example.com;

Poziom 3 – korzystanie z podpisów źródeł

To, co omówiliśmy wyżej, pozwala na definiowane zewnętrznych źródeł na bazie domeny. Możemy jednak chcieć korzystać ze skryptów zaszytych bezpośrednio w kodzie aplikacji. Również takie skrypty (lub style) mogą być zabezpieczone.Służą do tego dwa mechanizmy:

Podpis nonce

Dzięki temu możemy nazwać określony skrypt

<script nonce="randomData">
  // kod
</script>

by następnie pozwolić na wykonanie tylko skryptu o określonej nazwie.

Content-Security-Policy: script-src 'nonce-randomData'

Zalecane jest, aby nonce był całkowicie losowym ciągiem znaków, zmienianym co request. W przeciwnym przypadku niewiele nas on zabezpieczy.

Podpis hashem

Podobnie jak w poprzednim przypadku nazywaliśmy skrypt, możemy też wyliczyć jego hash (skrót)

<script>doSomething();</script>

a następnie umożliwić wykonywanie skryptów tylko o określonym podpisie.

script-src 'sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc=';

Dodatkowy wymiar – aktywne raportowanie naruszeń

Innym wymiarem konfiguracji CSP jest możliwość ustawienia raportowania wszelkich naruszeń. Dzięki temu podczas działania aplikacji będziemy informowani zarówno o próbach ataku jak i o naszych błędach w konfiguracji (kiedy zapomnimy zdefiniować określoną domenę). Możemy to zrobić, dodając sekcję:

report-uri https://endpoint.com; report-to groupname

Oczywiście musimy też mieć miejsce, gdzie będą logowane te naruszenia. Możemy w tym celu skorzystać z gotowych usług:

Jak również samodzielnie napisać odpowiedni endpoint w naszym API.

Praktyczne podejście

Jeżeli zaczynamy implementować CSP w naszej aplikacji, możemy to zrobić na dwa sposoby:

  • tylko raportując, 
  • blokując interakcje. 

Wybór odpowiedniej drogi zależy od poziomu skomplikowania aplikacji i ryzyka, jakie wiąże się z niezaładowaniem nie zdefiniowanych plików.

Jeżeli zaś chodzi o wartości ustawień CSP, to najlepiej zacząć od bazowego ustawienia.

default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self';base-uri 'self';form-action 'self'

Następnie należy zdefiniować wszystkie znane nam domeny, z których pobieramy dodatkowe zasoby.
Kolejnym krokiem jest uruchomienie strony, monitoring błędów w konsoli DevTools i poprawianie ustawień CSP.

Warto jeszcze wspomnieć, że sporo dostawców usług jasno definiuje zbiór opcji, który musi być dodany do CSP, aby integracja z określoną usługą działała poprawnie. Jako przykład zobaczmy:

Narzędzia do weryfikacji

Na zakończenie prac nad definiowaniem swoich ustawień CSP powinniśmy je zwalidować pod kątem poprawności i zaufania do domen. Można to zrobić za pomocą następujących narzędzi:

Warto to zrobić, bo może się okazać się, że domeny przez nas zdefiniowane nie są w pełni bezpieczne:

Przykład błędu
Przykład błędu

Podsumowanie

Content-Security-Policy to narzędzie o bardzo wielu opcjach. W prosty sposób jest w stanie zabezpieczyć nas przed wieloma zagrożeniami. Mnogość opcji nie powinna nas przytłaczać, bo na szczęście możemy implementować to zabezpieczenie na różnym poziomie dojrzałości. To nam pozwoli w szybki sposób osiągnąć oczekiwane efekty nawet w już istniejących aplikacjach. Dzięki temu jesteśmy w stanie łatwo i stopniowo dodawać CSP w gotowych aplikacjach.

Dodatkowe materiały: