Witajcie po małej przerwie spowodowanej natłokiem zadań związanych ze zmianą pracy, pragnieniu wypoczynku i planowaniu urlopu. Nazbierało się trochę tego wszystkiego i nie miałem głowy do kolejnego wpisu. Wybaczcie.
W związku z tym, że obiecałem Wam przykłady do artykułu o CSP, w niniejszym wpisie takie się znajdą. CSP to linia obrony przed kilkoma typami ataków na strony internetowe, więc zachęcam do sukcesywnego wdrażania jej w swoje projekty. Na pewno nic nie stracicie oprócz naprawdę kilku minut na dodanie ochrony i następnie konserwację zabezpieczenia.
Poniżej zaprezentuję zachowanie strony przy zastosowaniu określonych dyrektyw CSP 2.0. Pokażę wynik próby łamania zabezpieczenia (poprzez osoby z zewnątrz lub poprzez nieumyślne niedostosowanie źródeł dyrektywy).
Przykłady będę obrazować na wygenerowanym szablonie projektu opartego o Laravel 5.
1. base-uri
Kod strony:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title>Project 1</title> <link href='https://fonts.googleapis.com/css?family=Lato:100' rel='stylesheet' type='text/css'> <link href='style.css' rel='stylesheet' type='text/css'> <base href="http://localhost/images/"></base> </head> <body> <div class="container"><img src="laravel_5.jpg" /></div> <div class="container"> <div class="content"> <div class="title">Laravel 5.2.23 - brodząc w chmurach</div> </div> </div> </body> </html>
Zwróćcie proszę uwagę na znacznik <base> w nagłówku i dodane zdjęcie ze źródłem, jako adres względny. Wszystko wygląda elegancko, ale czy nie ułatwiamy w ten sposób manipulacji zasobami (zwłaszcza, gdy większość lub wszystkie określamy adresami względnymi)? Owszem. Wystarczy złośliwy monkey script w naszej przeglądarce i możemy zostać „zasypani” zasobami z zewnątrz.
Pominięcie znacznika <base> i ustalenie źródła obrazka na „images/laravel_5.jpg” pozwoli osiągnąć ten sam cel, ale tak czy inaczej <base> można dodać w trakcie wykonywania skryptów.
Efekt działania strony poniżej:
Taki sam efekt daje ustawienie dyrektywy CSP na źródło ‚self’, gdyż pozwalamy na ustawienie bazowego źródła na domenę własną zgodnie z zasadą same origin policy.
header("Content-Security-Policy: base-uri 'self'");
Jeśli jednak zmienimy w tym przypadku źródło na ‚none’ – brak przyzwolenia na jakiekolwiek zmiany, otrzymamy taki rezultat (proszę spojrzeć na ostrzeżenie w konsoli JavaScript u dołu obrazka):
Manipulacja źródłem w tym przypadku, jak i następnych przykładam należy do zadania programisty i zależy od potrzeb.
2. child-src
Kod strony:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title>Project 1</title> <link href='https://fonts.googleapis.com/css?family=Lato:100' rel='stylesheet' type='text/css'> <link href='style.css' rel='stylesheet' type='text/css'> </head> <body> <div class="container"><iframe src="http://localhost" style="width: 400px; height: 400px;"></iframe></div> <div class="container"> <div class="content"> <div class="title">Laravel 5.2.23 - brodząc w chmurach</div> </div> </div> </body> </html>
Tutaj na potrzeby dyrektywy występuje znacznik <iframe>.
Rezultat:
Ustalenie dyrektywy na:
header("Content-Security-Policy: child-src 'none'");
spowoduje wyświetlenie komunikatu w konsoli i zablokowanie treści ramki.
3. connect-src
Kod strony:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title>Project 1</title> <link href='https://fonts.googleapis.com/css?family=Lato:100' rel='stylesheet' type='text/css'> <link href='style.css' rel='stylesheet' type='text/css'> <script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script> <script>$.get('http://localhost/version', function(data) { $('.title>span').text(' '+data); });</script> </head> <body> <div class="container"> <div class="content"> <div class="title">Laravel<span></span> - brodząc w chmurach</div> </div> </div> </body> </html>
Tutaj dodałem zewnętrzny zasób jQuery w celu łatwego poruszania się manipulowania węzłami strony (już z przyzwyczajenia wykorzystuję). Jednak nie on jest najważniejszy. Dyrektywa odpowiada za ajaxowe żądania i tutaj wykorzystuję krótki skrypt, gdzie funkcją ajaxową $.get pobieram z serwera wersję i wstrzykuję na stronę do węzła span. Przykład prosty, obrazowy:
Jeśli zablokujemy jak źródło żądań ajaxowych lub zmienimy wartość niepozwalającą na wywołanie adresu /version, wersja Laravela nie ukaże nam się na ekranie – żądanie zostanie zablokowane.
Dyrektywa:
header("Content-Security-Policy: connect-src 'none'");
Rezultat:
4. default-src
Szczególna dyrektywa. Dla kodu z poprzedniego przykładu i ustawienia wyłącznie dyrektywy default-src na ‚self’ spowoduje wyświetlenie mało obiecującego rezultatu:
Widzimy brak czcionki, brak wywołania $.get, co nie powinno nas dziwić – w końcu nie ustawiliśmy pozwolenia na skrypt inline, a także do doładowanie czcionki z serwerów google (oraz biblioteki jQuery z CDN). Natomiast style CSS zostały doładowane bez problemu z pliku style.css. Aby wszystko ładnie wyglądało należałoby dodać dyrektywę font-src i script-src (by umożliwić doładowanie zasobów i wywołanie skryptu inline). Rozwiązanie tego przykładu znajdziesz w następnych częściach artykułu.
5. font-src
Tutaj również wykorzystamy kod z przykładu 3. Gwoli wyjaśnienia już tutaj dyrektywa default-src NIE będzie wykorzystywana.
Poniżej efekt dla dyrektywy:
header("Content-Security-Policy: font-src 'self'");
Wszystko w porządku oprócz czcionki. Nie ma czemu się dziwić – udostępniliśmy wyłącznie czcionki tzw. wewnętrzne.
Naprawić to możemy dodając źródło naszej czcionki:
header("Content-Security-Policy: font-src 'self' https://fonts.gstatic.com/s/lato/");
Tutaj jest akurat śliski temat, gdyż czcionkę dodajemy przez <link> do arkusza stylów, gdzie znajduje się adres do czcionki. Gdyby Google zmienił serwer, domenę lub ścieżkę do zasobu czcionka nie zostałaby doładowana na stronie z wyżej wskazanym zabezpieczeniem.
6. form-action
Zabezpieczenie akcji formularza, ważne gdyż chroni przez wysłaniem danych wrażliwych.
Kod strony:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title>Project 1</title> <link href='https://fonts.googleapis.com/css?family=Lato:100' rel='stylesheet' type='text/css'> <link href='style.css' rel='stylesheet' type='text/css'> <script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script> <script>$.get('http://localhost/version', function(data) { $('.title>span').text(' '+data); });</script> </head> <body> <div class="container" style="width: 600px"> <div>FORMULARZ</div> <form action="/form"> <input name="comment" type="text"></input> <input type="submit" name="submit" value="Wyślij"></input> </form> </div> <div class="container"> <div class="content"> <div class="title">Laravel<span></span> - brodząc w chmurach</div> </div> </div> </body> </html>
Pojawia się formularz z ustawionym adresem akcji na /form. Wysyłka takiego formularza przy braku zabezpieczeń daje następujący efekt:
Natomiast, gdy uniemożliwimy wysyłkę danych z formularza (źródło ‚none’ lub ustawimy na inne niż wymaga tego atrybut action węzła form):
header("Content-Security-Policy: form-action 'none'");
otrzymamy brak reakcji na submit formularza poparty odpowiednimi alertami w konsoli JS:
Przeglądarka blokuje już samą inicjalizację wysyłki danych, co wygląda obiecująco, ale PAMIĘTAJ:
Wykorzystywana przez użytkownika przeglądarka może być na tyle prymitywna, że nie będzie obsługiwać zabezpieczeń CSP.
7. frame-ancestors
Bardzo ciekawa dyrektywa ze względu na tzw. odwrotny przepływ – w węźle zagnieżdżony następuje sprawdzanie nagłówka w poszukiwaniu źródła i jeśli strona z ramką jest „na białej liście” zasób jest renderowany w ramce, w przeciwnym wypadku jest blokowany.
Wykorzystam kod z przykładu 3. Jednak nieco go zmodyfikuję – usunięcie żądania ajaxowego, zbędnych bibliotek i skryptów. Zamiast tego w miejscu znacznika <span> dodam <iframe>.
Kod strony:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title>Project 1</title> <link href='https://fonts.googleapis.com/css?family=Lato:100' rel='stylesheet' type='text/css'> <link href='style.css' rel='stylesheet' type='text/css'> </head> <body> <div class="container"> <div class="content"> <div class="title">Laravel<iframe src="/version" style="width: auto; height: 1.5em; vertical-align: middle; padding: 0px; border: 0;"></iframe> - brodząc w chmurach</div> </div> </div> </body> </html>
Rezultat:
Jeśli natomiast ustawimy, że dla żądania zasobu spod adresu /version nie ma być akceptowane wyświetlanie w ramkach to efekt będzie następujący:
Nastąpiło zablokowanie żądania z ramki, ale dostęp do zasobu poprzez żądanie np. ajaxowe lub bezpośrenio jest możliwe:
8. img-src
Kod z przykładu 1. po drobnych zmianach:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title>Project 1</title> <link href='https://fonts.googleapis.com/css?family=Lato:100' rel='stylesheet' type='text/css'> <link href='style.css' rel='stylesheet' type='text/css'> </head> <body> <div class="container"><img src="/images/laravel_5.jpg" /></div> <div class="container"> <div class="content"> <div class="title">Laravel 5.2.23 - brodząc w chmurach</div> </div> </div> </body> </html>
Rezultat:
Jeśli ustalimy źródło dyrektywny na katalog /img, a nie /images możemy się domyślić, że obrazek się nie wyświetli.
Dyrektywa:
header("Content-Security-Policy: img-src http://localhost/img/");
UWAGA:
Wskazane źródło – określenie konkretnego katalogu zasobu (przynajmniej to) nie działa poprawnie w przeglądarce Opera 37.0.2178.43. Źródło wygląda jakby nie było poprawnie walidowane i przeglądarka uznaje je jako niepoprawne – tym samym dyrektywa jest ignorowana obrazek zostaje wyświetlony. Przeglądarka Chrome 50.0.2661.102 reaguje zgodnie z założeniami.
Rezultat Opera:
Rezultat Chrome:
9. script-src
Znowu skorzystamy w całości z kodu z przykładu 3. Ustawienie źródła dyrektywy na ‚self’ da nam podobny rezultat jak w przykładzie 4. z ustawioną dyrektywą default-src na ‚self’ – jedyną różnicą będzie doładowana czcionka z serwerów Google. Nie zgadzamy się na zewnętrzne skrypty i skrypty inline – rezultat:
Komunikat w konsoli mówi jasno o możliwości włączenia skryptu inline, a właściwie trzech możliwościach: unsafe-inline, z wykorzystaniem hasha lub prefiksa nonce-.
Konsekwentnie nie zalecam stosowanie ani unsafe-inline w źródle, ani unsafe-eval, gdyż bardzo obniżają zabezpieczenie przed XSS.
Poprawna dyrektywa w tym przykładzie wygląda tak:
header("Content-Security-Policy: script-src 'self' 'sha256-OoqMnsRI8uSyHmWVsgnin9QjWb++7azPDSAbJHAZ5C0=' https://code.jquery.com/jquery-2.2.4.min.js");
Rezultat:
Tutaj mimo poprawnego działania otrzymujemy błąd w konsoli. Uspokajam – jest on związany w włączoną w przeglądarce wtyczką Violentmonkey, która pozwala manipulować skryptami własnymi podczas ładowania stron – tzw. monkey scripts.
Jak widać dyrektywa ma jeszcze jeden pozytywny skutek, mianowicie blokuje manipulację stroną poprzez takie skrypty, co może się przydawać, gdy projekt np. bazuje na silniku klienta i wszelkie manipulacje użytkownika są z punktu widzenia biznesowego projektu niepożądane.
10. style-src
Bazujmy znowu na kodzie z przykładu 3. Dla dyrektywy ustawmy źródło ‚none’. Efekt poniżej:
Ok, konsola mówi nam, co jest nie tak, jakie style zostały zablokowane. Zmiana na źródło ‚self’ nie przywróci nam czcionki, która jest ładowana wraz ze stylami Google.
Zmieniamy dyrektywę na:
header("Content-Security-Policy: style-src 'self' https://fonts.googleapis.com/css?family=Lato:100");
Rezultat:
Wszystko wygląda, jak należy.
Mam nadzieję, że na tych prymitywnych przykładach pokazałem Wam, co i jak w temacie CSP. Wnioski, jakie powinniście wyciągnąć z artykułu są następujące:
- obsługa i wsparcie dyrektyw zależy od przeglądarek i ich wersji
- nigdy nie stosujcie źródeł unsafe-inline ani unsafe-eval
- domyślne źródła ‚self’ jest z bardzo dużym prawdopodobieństwem bezpiecznym rozwiązaniem
- jeśli czujesz obawy przed stosowanie dyrektyw w portalu, który przed chwilą wdrożyłeś, ale chciałbyś je dodać, skoncentruj się wyłącznie na trybie raportowania
Osoby, które jeszcze za bardzo nie rozumieją zasady działania CSP lub poszczególnych dyrektyw przekierowuję do poprzedniego artykułu.
Pozdrawiam!
Pingback: CSP – sposób na XSS, Clickjacking, CSS-Injection i inne – Po-słowie na każdy temat
Pingback: CSP – tryb raportowania – Po-słowie na każdy temat