08-09-2019 / Od 0 do pentestera

Trusted Types - ochrona przed DOM Based XSS

W dzisiejszym odcinku "od 0 do pentestera" o eksperymentalnej opcji Trusted Types, której celem jest rozwiązanie problemu z atakami typu Dom Based XSS.

Co to za atak i jak działa?

W tradycyjnej podatność Cross Site Scripting - dane przesłane przez użytkownika są pobierane i wyświetlane przez serwer bez odpowiedniej weryfikacji i walidacji.

To sprawia, że atakujący zamiast danych których oczekuje skrypt może podać złośliwy kod HTML, który to wykona się w obrębie danej domeny.

Jeżeli odwiedzający witrynę kontroluje kod JavaScript, który jest przez nią wyświetlany - może w ten sposób pobrać ciasteczka czy tez wykonać jakąś akcję w imieniu użytkownika.

Dom Based XSS

Nieco inaczej wygląda to w przypadku błędu typu Dom Based.

Tutaj bowiem złośliwy kod nie jest generowany po stronie serwera, a po stronie kodu JavaScript, który zawiera jakąś podatność.

Ale aby do tego doszło muszą połączyć się 2 elementy.

Pierwszy z nich to sources.

Jest to jakaś wartość - kontrolowana przez użytkownika, która może być pobrana z poziomu kodu JavaScript.

Najczęściej jest to location.hash - czyli treść adresu URL przekazanego przez użytkownika, która zaczyna się od znaku #.

Location.hash

Najprościej wytłumaczyć to na przykładzie.

<script>
window.onload = function() {
	var hash = location.hash.substring(1);
	document.getElementById("number").innerHTML = decodeURIComponent(hash);
};
</script>
Twoja liczba: <div id="number"></div>

Mamy zatem prosty skrypt, który wyświetla liczbę podaną przez użytkownika w adresie URL.

Skrypt ten pobiera wartość location.hash a następnie przekazuje ją do elementu number - ustawiając ją tam jako kod HTML.

Dzięki temu modyfikując wartość w adresie zmienia się treść wyświetlana na stronie internetowej.

Jest to więc coś bardzo podobnego do wartości GET, które przesyłane są do serwera.

Mamy więc pierwszy potrzebny element.

Skrypt pobiera jakąś wartość od użytkownika.

Teraz musi coś z nią zrobić.

Tutaj do gry wchodzi sink - czyli takie miejsce, które wykonuje operację na pobranych wcześniej danych.

JS sink

JS zawiera bowiem funkcji i właściwości, które nie koniecznie są bezpieczne.

Niektóre z nich pozwalają bowiem na stworzenie nowego kodu JS w obrębie danej domeny.

Tworzenie kodu na podstawie danych od użytkownika prowadzi do błędów bezpieczeństwa.

Dlatego tez powinniśmy unikać konstrukcji innerHTML czy też document.write, które są w stanie zamienić tekst na kod HTML wykonywany w kontekście danej strony.

Poszukiwanie błędów DOM Based polega zatem na odnajdywaniu takich miejsc w skrypcie strony, które pobierają danej od użytkownika a następnie przekazują je do potencjalnie niebezpiecznych miejsc w stylu innerHTML bez odpowiedniej weryfikacji.

Jak zatem firmy próbują się chronić przed tego rodzaju podatnościami?

Jednym z popularnych rozwiązań jest używanie bezpiecznych alternatyw dla podanych wcześniej potencjalnie niebezpiecznych operacji.

Alternatywa dla innerHTML

I tak dla innerHTML mamy innerText.

Ta właściwość ustawia podaną jako parametr wartość dla konkretnego elementu na stronie nie jako kod HTML a jako zwykły tekst.

Podany parametr zatem nie zostanie automatycznie zamieniony na kod a po prostu zwyczajnie wyświetlony na stronie.

A wyświetlanie kodu jako tekst to nic złego.

Nie jest on bowiem interpretowany przez przeglądarkę a traktowany jako zwykły zlepek słów.

Niestety takie rozwiązanie ma swoje minusy.

Minusy innerText

Czasami istnieją sytuację, w których programista musi na podstawie danych od użytkownika stworzyć jakiś kod HTML.

Zmodyfikujmy lekko nasz przykład.

Tym razem nie chcemy wyświetlać liczby podanej jako parametr ale na jej podstawie stworzyć odnośnik do zewnętrznej strony.

Tym razem musimy użyć składni HTML.

<script>
window.onload = function() {
	var number = decodeURIComponent(location.hash.substring(1));
	var link = '<a href="http://google.pl">Link '+number+'</a>';
	document.getElementById("number").innerHTML = link; 
};
</script>
Twoja liczba: <div id="number"></div>

Wszystko działa prawidłowo jeżeli użytkownik podaje same liczby.

Ale przecież nic nie stoi na przeszkodzi, aby podał tekst - również taki, który jest złośliwy.

Wtedy to jego ciąg znaków przekazany w adresie URL zostanie automatycznie zamieniony na kod HTML.

Reflected XSS

Aby się przed tym obronić musimy zatem sprawdzić, czy przekazywany parametr jest liczbą i wyłącznie wtedy wyświetlać go na naszej stronie.

Teoretycznie nie jest to skomplikowane:

<script>
window.onload = function() {
	var number = decodeURIComponent(location.hash.substring(1));
	if (/^[0-9]+$/.test(number)) {
		var link = '<a href="http://google.pl">Link '+number+'</a>';
		document.getElementById("number").innerHTML = link; 
	}
};
</script>
Twoja liczba: <div id="number"></div>

Możemy tego dokonać chociażby przy użyciu wyrażeń regularnych sprawdzając, czy ciąg zawiera jedynie cyfry.

Tylko, że tutaj mamy do czynienia z prostym przykładem.

Wiemy gdzie pobierany jest parametr od użytkownika a także gdzie jest on wyświetlany na stronie.

W normalnym życiu wcale nie jest tak prosto.

Problemy w realnym kodzie

Parametry mogą być pobierane w wielu miejscach naszego kodu

Mogą być także przekazywane przez wiele dodatkowych funkcji i wyświetlane w potencjalnie wielu miejscach.

Szybko więc możemy nad tym wszystkim stracić kontrolę.

A wystarczy, że zapomnimy o jednym miejscu i cały nasz wysiłek spełznie na niczym.

I tutaj do gry wchodzi standard Trusted Types.

Jest to względnie nowa rzecz.

Jeżeli więc chciałbyś przetestować podane tutaj przykłady na swoim komputerze, musisz najpierw włączyć odpowiednia opcję.

Nie jest to trudne.

Jak wlączyć Trusted Types w Chrome

Wystarczy włączyć Chrome z odpowiednim przełącznikiem albo ustawić odpowiednia opcję w zakładce Flags:

chrome://flags/#enable-experimental-web-platform-features

Następnie musimy ustawić odpowiedni nagłówek CSP z wartością trusted-types.

Na czym więc polega cały pomysł tego zabezpieczenia?

Zauważono, że cały problem błędów DOM Based polega na tym, że w pewnym momencie nieodpowiednio przetworzona wartość od użytkownika jest przekazywana do niebezpiecznych miejsc.

Jeżeli zatem zabronimy przekazywania dowolnych danych do tych elementów - może to być genialne w swojej prostocie rozwiązanie.

Idea Trusted Types

Zmieniono zatem silnik przeglądarki w taki sposób, aby wszystkie potencjalnie niebezpieczne miejsca, które mogą posłużyć do wykonania kodu JS, przestały przyjmować jako parametr ciąg znaków.

Zamiast tego, akceptują jedynie obiekt TrustedHTML bądź inny, podobny w zależności od przyjmowanych parametrów.

TrustedHTML

Standard przewiduje bowiem TrustedURL dla odnośników czy też TrustedScript dla kodu JS.

Jeżeli programista przez przypadek przekaże tam co innego, przeglądarka zwróci odpowiedni błąd i nie doprowadzi do wykonania danej instrukcji.

On to jest tworzony przy pomocy nowego API createPolicy.

Tam to definiujemy politykę, której zadaniem jest sprawdzenie - czy parametr do niej przekazany jest odpowiedniego typu oraz czy nie zawiera złośliwego kodu JavaScript.

const templatePolicy = TrustedTypes.createPolicy('polityka', {
  createHTML: (number) => {
  	if (/^[0-9]+$/.test(number)) {
    	return "<a href='http://google.pl'>Link "+number+"</a>";
	}
  }
});

Jeżeli wartość jest prawidłowa - to właśnie tam tworzymy ciąg tekstowy, który to zostanie przekazany do niebezpiecznych miejsc.

Użycie tego mechanizmu wymaga od nas pewnych zmian w kodzie.

Wszystkie potencjalnie niebezpiecznych miejscach bowiem, musza teraz przyjmować obiekt TrustedHTML.

<?php
header('Content-Security-Policy: trusted-types polityka');
<script>
const templatePolicy = TrustedTypes.createPolicy('polityka', {
  createHTML: (number) => {
  	if (/^[0-9]+$/.test(number)) {
    	return "<a href='http://google.pl'>Link "+number+"</a>";
	}
  }
});
window.onload = function() {
	var number = decodeURIComponent(location.hash.substring(1));	
	document.getElementById("number").innerHTML = templatePolicy.createHTML(number);
};
</script>
Twoja liczba: <div id="number"></div>

Możesz się teraz zastanawiać.

Dlaczego nie mogę samemu stworzyc obiektu TrustedHTML a musze korzystać ze specjalnie przygotowanego do tego celu API?

Całość mogła by bowiem wyglądać mniej więcej tak:

<?php
header('Content-Security-Policy: trusted-types polityka');
?>
<script>
window.onload = function() {
    class TrustedHTML {
	    constructor(s) {
			this.s = s      
	    }
	    toString() {
	      return this.s;
	    }
	}
	var number = new TrustedHTML(location.hash.substring(1));
	document.getElementById("number").innerHTML = number;
};
</script>
Twoja liczba: <div id="number"></div>

Każdy programista mógłby wtedy tworzyć własny obiekt Trusted przekazywany do odpowiednich sinków.

Czyli nadal niewiele by się zmieniło.

Znowu wprowadzamy chaos ponieważ dalej nie wiadomo jak wygląda przepływ danych w aplikacji.

Osoba odpowiedzialna za bezpieczeństwo musiała by ponownie sprawdzać wszystkie niebezpieczne miejsca.

Nigdy nie miała by bowiem pewności które klasy są odpowiedzialne za weryfikacje danego parametru.

Bezpieczeństwo Trusted Types

Jeżeli jednak programista jest zmuszony do używania API createpolicy całość wygląda z goła inaczej.

Osoby zajmujące się security muszą jedynie sprawdzić bezpieczeństwo kodu zdefiniowanego tylko tam.

Nawet jeżeli gdzieś przez przypadek przekazano zwykły ciąg tekstowy - przeglądarka go nie wykona.

Co więcej nie trzeba teraz sprawdzać wszystkich wartości source bowiem i tak w ostatecznym rozrachunku te wartości będą usiały trafić do kodu polityki.

Jeżeli zatem zwracany przez politykę kod jest bezpieczny - pozbyliśmy się problemu z Dom Based XSS.

Wiele polityk Trusted Types

Każda strona może posiadać wiele polityk.

To pozwala na ich ponowne używanie w przyszłości.

Raz stworzone polityki mogą być następnie używane w wielu miejscach.

Co więcej nawet jeżeli odnaleziony zostanie błąd - wystarczy zmienić go tylko w polityce.

Nie ma potrzeby zmiany reszty aplikacji.

Obsługa wielu polityk Trusted Types

Co więcej właściciel witryny ma pełna kontrolę nad tym, które polityki mogą działać w obrębie danej domeny.

W nagłówku bowiem można przesłać gwiazdkę - pozwalając na wiele polityk, lub też ich konkretne nazwy.