Latte jest synonimem bezpieczeństwa

Latte to jedyny system szablonów dla PHP ze skuteczną ochroną przed krytyczną podatnością Cross-site Scripting (XSS). A to dzięki tzw. escapowaniu kontekstowemu. Opowiemy o tym,

  • jaka jest zasada podatności XSS i dlaczego jest tak niebezpieczna
  • dlaczego Latte jest tak skuteczne w obronie przed XSS
  • jak łatwo można zrobić lukę bezpieczeństwa w szablonach Twig, Blade i innych

Cross-site Scripting (XSS)

Cross-site Scripting (w skrócie XSS) jest jedną z najczęstszych podatności stron internetowych, a jednocześnie bardzo niebezpieczną. Pozwala atakującemu wstrzyknąć do obcej strony złośliwy skrypt (tzw. malware), który uruchomi się w przeglądarce niczego nieświadomego użytkownika.

Co może zrobić taki skrypt? Może na przykład wysłać atakującemu dowolną zawartość z zaatakowanej strony, w tym wrażliwe dane wyświetlane po zalogowaniu. Może zmodyfikować stronę lub wykonywać kolejne żądania w imieniu użytkownika. Jeśli na przykład byłby to webmail, może odczytać wrażliwe wiadomości, zmodyfikować wyświetlaną zawartość lub zmienić konfigurację, np. włączyć przekazywanie kopii wszystkich wiadomości na adres atakującego, aby uzyskać dostęp również do przyszłych e-maili.

Dlatego też XSS figuruje na czołowych miejscach rankingów najniebezpieczniejszych podatności. Jeśli na stronie internetowej pojawi się podatność, należy ją jak najszybciej usunąć, aby zapobiec nadużyciom.

Jak powstaje podatność?

Błąd powstaje w miejscu, gdzie generowana jest strona internetowa i wyświetlane są zmienne. Wyobraź sobie, że tworzysz stronę z wyszukiwaniem, a na początku będzie akapit z szukanym wyrażeniem w postaci:

echo '<p>Wyniki wyszukiwania dla <em>' . $search . '</em></p>';

Atakujący może w polu wyszukiwania, a tym samym w zmiennej $search, wpisać dowolny ciąg znaków, czyli również kod HTML, taki jak <script>alert("Hacked!")</script>. Ponieważ wyjście nie jest w żaden sposób oczyszczone, stanie się częścią wyświetlonej strony:

<p>Wyniki wyszukiwania dla <em><script>alert("Hacked!")</script></em></p>

Przeglądarka zamiast wyświetlić szukany ciąg, uruchomi JavaScript. I tym samym kontrolę nad stroną przejmuje atakujący.

Można argumentować, że wstrzyknięcie kodu do zmiennej spowoduje uruchomienie JavaScriptu, ale tylko w przeglądarce atakującego. Jak dotrze do ofiary? Z tego punktu widzenia rozróżniamy kilka typów XSS. W naszym przykładzie z wyszukiwaniem mówimy o reflected XSS. Tutaj trzeba jeszcze nakłonić ofiarę, aby kliknęła link, który będzie zawierał złośliwy kod w parametrze:

https://example.com/?search=<script>alert("Hacked!")</script>

Nakłonienie użytkownika do kliknięcia linku wymaga pewnego inżynierii społecznej, ale nie jest to nic skomplikowanego. Użytkownicy klikają linki, czy to w e-mailach, czy na portalach społecznościowych, bez większego zastanowienia. A to, że w adresie jest coś podejrzanego, można zamaskować za pomocą skracacza URL, użytkownik widzi wtedy tylko bit.ly/xxx.

Istnieje jednak również druga i znacznie bardziej niebezpieczna forma ataku, określana jako stored XSS lub persistent XSS, w której atakującemu udaje się zapisać złośliwy kod na serwerze tak, aby był automatycznie wstawiany do niektórych stron.

Przykładem są strony, na których użytkownicy piszą komentarze. Atakujący wysyła post zawierający kod, który zostaje zapisany na serwerze. Jeśli strony nie są wystarczająco zabezpieczone, będzie się on uruchamiał w przeglądarce każdego odwiedzającego.

Mogłoby się wydawać, że sedno ataku polega na wstrzyknięciu do strony ciągu <script>. W rzeczywistości sposobów wstrzyknięcia JavaScriptu jest wiele. Pokażemy na przykład wstrzyknięcie za pomocą atrybutu HTML. Miejmy galerię zdjęć, gdzie można dodawać do obrazków opis, który zostanie wyświetlony w atrybucie alt:

echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';

Atakującemu wystarczy jako opis wstawić sprytnie skonstruowany ciąg " onload="alert('Hacked!'), a jeśli wyświetlanie nie zostanie oczyszczone, wynikowy kod będzie wyglądał tak:

<img src="photo0145.webp" alt="" onload="alert('Hacked!')">

Częścią strony staje się teraz podrzucony atrybut onload. Przeglądarka kod w nim zawarty uruchomi zaraz po pobraniu obrazka. Hacked!

Jak bronić się przed XSS?

Wszelkie próby wykrycia ataku za pomocą czarnej listy, takie jak blokowanie ciągu <script> itp., są niewystarczające. Podstawą skutecznej obrony jest konsekwentna sanityzacja wszystkich danych wyświetlanych wewnątrz strony.

Przede wszystkim chodzi o zastąpienie wszystkich znaków o specjalnym znaczeniu innymi odpowiadającymi sekwencjami, co potocznie nazywa się escapowaniem (pierwszy znak sekwencji nazywa się znakiem ucieczki, stąd nazwa). Na przykład w tekście HTML specjalne znaczenie ma znak <, który, jeśli nie ma być interpretowany jako początek znacznika, musimy zastąpić wizualnie odpowiadającą sekwencją, tzw. encją HTML &lt;. A przeglądarka wyświetli znak mniejszości.

Bardzo ważne jest rozróżnianie kontekstu, w którym dane wyświetlamy. Ponieważ w różnych kontekstach ciągi znaków są różnie sanityzowane. W różnych kontekstach specjalne znaczenie mają różne znaki. Na przykład różni się escapowanie w tekście HTML, w atrybutach HTML, wewnątrz niektórych specjalnych elementów itp. Za chwilę omówimy to szczegółowo.

Oczyszczanie najlepiej przeprowadzać bezpośrednio przy wyświetlaniu ciągu na stronie, co zapewnia, że zostanie ono rzeczywiście wykonane i wykonane dokładnie raz. Najlepiej, jeśli oczyszczanie zapewnia automatycznie sam system szablonów. Ponieważ jeśli oczyszczanie nie odbywa się automatycznie, programista może o nim zapomnieć. A jedno przeoczenie oznacza, że strona jest podatna na ataki.

Jednak XSS dotyczy nie tylko wyświetlania danych w szablonach, ale także innych części aplikacji, które muszą poprawnie obsługiwać niezaufane dane. Na przykład konieczne jest, aby JavaScript w Twojej aplikacji nie używał w związku z nimi innerHTML, ale tylko innerText lub textContent. Szczególną uwagę należy zwrócić na funkcje, które oceniają ciągi jako JavaScript, takie jak eval(), ale także setTimeout(), ewentualnie użycie funkcji setAttribute() z atrybutami zdarzeń, takimi jak onload itp. To już jednak wykracza poza obszar objęty szablonami.

Idealna obrona w 3 punktach:

  1. rozpoznaje kontekst, w którym dane są wyświetlane
  2. sanityzuje dane zgodnie z zasadami danego kontekstu (czyli „kontekstowo”)
  3. robi to automatycznie

Escapowanie kontekstowe

Co dokładnie oznacza słowo kontekst? Jest to miejsce w dokumencie z własnymi zasadami oczyszczania wyświetlanych danych. Zależy od typu dokumentu (HTML, XML, CSS, JavaScript, plain text, …) i może się różnić w jego poszczególnych częściach. Na przykład w dokumencie HTML istnieje wiele takich miejsc (kontekstów), gdzie obowiązują bardzo różne zasady. Być może będziesz zaskoczony, ile ich jest. Oto pierwsza czwórka:

<p>#text</p>
<img src="#atrybut">
<textarea>#rawtext</textarea>
<!-- #komentarz -->

Domyślnym i podstawowym kontekstem strony HTML jest tekst HTML. Jakie tu obowiązują zasady? Specjalne znaczenie mają znaki < i &, które reprezentują początek znacznika lub encji, więc musimy je escapować, zastępując je encją HTML (< na &lt; & na &amp).

Drugim najczęstszym kontekstem jest wartość atrybutu HTML. Różni się od tekstu tym, że specjalne znaczenie ma tu cudzysłów " lub ', który ogranicza atrybut. Należy go zapisać jako encję, aby nie był rozumiany jako koniec atrybutu. Natomiast w atrybucie można bezpiecznie używać znaku <, ponieważ tutaj nie ma on żadnego specjalnego znaczenia, tutaj nie może być rozumiany jako początek znacznika czy komentarza. Ale uwaga, w HTML można pisać wartości atrybutów również bez cudzysłowów, w takim przypadku specjalne znaczenie ma cała gama znaków, jest to więc kolejny oddzielny kontekst.

Być może Cię to zaskoczy, ale specjalne zasady obowiązują wewnątrz elementów <textarea> i <title>, gdzie znak < nie musi (ale może) być escapowany, jeśli nie następuje po nim /. Ale to raczej ciekawostka.

Ciekawie jest wewnątrz komentarzy HTML. Tutaj bowiem do escapowania nie używa się encji HTML. Nawet żadna specyfikacja nie podaje, jak powinno się escapować w komentarzach. Trzeba tylko przestrzegać nieco ciekawych zasad i unikać w nich pewnych kombinacji znaków.

Konteksty mogą się również nakładać, co ma miejsce, gdy wstawiamy JavaScript lub CSS do HTML. Można to zrobić na dwa różne sposoby, elementem i atrybutem:

<script>#js-element</script>
<img onclick="#js-atrybut">

<style>#css-element</style>
<p style="#css-atrybut"></p>

Dwie ścieżki i dwa różne sposoby escapowania danych. Wewnątrz elementu <script> i <style>, podobnie jak w przypadku komentarzy HTML, escapowanie za pomocą encji HTML nie jest przeprowadzane. Przy wyświetlaniu danych wewnątrz tych elementów należy przestrzegać jednej zasady: tekst nie może zawierać sekwencji </script odpowiednio </style.

Natomiast w atrybutach style i on*** escapuje się za pomocą encji HTML.

I oczywiście wewnątrz zagnieżdżonego JavaScriptu lub CSS obowiązują zasady escapowania tych języków. Zatem ciąg w atrybucie np. onload jest najpierw escapowany zgodnie z zasadami JS, a następnie zgodnie z zasadami atrybutu HTML.

Uff… Jak widzisz, HTML jest bardzo złożonym dokumentem, w którym nakładają się konteksty, i bez świadomości, gdzie dokładnie dane wyświetlam (tj. w jakim kontekście), nie można powiedzieć, jak to poprawnie zrobić.

Chcesz przykład?

Miejmy ciąg Rock'n'Roll.

Jeśli będziesz go wyświetlać w tekście HTML, akurat w tym przypadku nie trzeba dokonywać żadnych zamian, ponieważ ciąg nie zawiera żadnego znaku o specjalnym znaczeniu. Inna sytuacja nastąpi, jeśli wyświetlisz go wewnątrz atrybutu HTML ujętego w pojedyncze cudzysłowy. W takim przypadku trzeba escapować cudzysłowy na encje HTML:

<div title='Rock&apos;n&apos;Roll'></div>

To było proste. Znacznie ciekawsza sytuacja nastąpi przy nakładaniu kontekstów, na przykład jeśli ciąg będzie częścią JavaScriptu.

Najpierw więc wyświetlimy go w samym JavaScripcie. Tj. opakujemy go w cudzysłowy i jednocześnie escapujemy za pomocą znaku \ cudzysłowy w nim zawarte:

'Rock\'n\'Roll'

Możemy jeszcze uzupełnić wywołanie jakiejś funkcji, żeby kod coś robił:

alert('Rock\'n\'Roll');

Jeśli ten kod wstawimy do dokumentu HTML za pomocą <script>, nie trzeba niczego więcej modyfikować, ponieważ nie występuje w nim zakazana sekwencja </script:

<script> alert('Rock\'n\'Roll'); </script>

Jeśli jednak chcielibyśmy go wstawić do atrybutu HTML, musimy jeszcze escapować cudzysłowy na encje HTML:

<div onclick='alert(&apos;Rock\&apos;n\&apos;Roll&apos;)'></div>

Zagnieżdżonym kontekstem nie musi być jednak tylko JS lub CSS. Często jest nim również URL. Parametry w URL escapuje się tak, że znaki o specjalnym znaczeniu konwertuje się na sekwencje zaczynające się od %. Przykład:

https://example.org/?a=Jazz&b=Rock%27n%27Roll

A kiedy ten ciąg wyświetlimy w atrybucie, jeszcze zastosujemy escapowanie zgodnie z tym kontekstem i zastąpimy & na &amp:

<a href="https://example.org/?a=Jazz&amp;b=Rock%27n%27Roll">

Jeśli doczytałeś aż dotąd, gratulujemy, było to wyczerpujące. Teraz już masz dobre pojęcie o tym, czym są konteksty i escapowanie. I nie musisz się martwić, że to skomplikowane. Latte robi to bowiem za Ciebie automatycznie.

Latte vs naiwne systemy

Pokazaliśmy, jak poprawnie escapuje się w dokumencie HTML i jak kluczowa jest znajomość kontekstu, czyli miejsca, gdzie dane wyświetlamy. Innymi słowy, jak działa escapowanie kontekstowe. Chociaż jest to niezbędny warunek skutecznej obrony przed XSS, Latte jest jedynym systemem szablonów dla PHP, który to potrafi.

Jak to możliwe, skoro wszystkie systemy dzisiaj twierdzą, że mają automatyczne escapowanie? Automatyczne escapowanie bez znajomości kontekstu to trochę bullshit, który tworzy fałszywe poczucie bezpieczeństwa.

Systemy szablonów, takie jak Twig, Laravel Blade i inne, nie widzą w szablonie żadnej struktury HTML. Nie widzą więc również kontekstów. W porównaniu do Latte są ślepe i naiwne. Przetwarzają tylko własne znaczniki, wszystko inne jest dla nich nieistotnym strumieniem znaków:

░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░
░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░
- w tekście: <span>{{ foo }}</span>
- w tagu: <span {{ foo }} ></span>
- w atrybucie: <span title='{{ foo }}'></span>
- w atrybucie bez cudzysłowów: <span title={{ foo }}></span>
- w atrybucie zawierającym URL: <a href="{{ foo }}"></a>
- w atrybucie zawierającym JavaScript: <img onload="{{ foo }}">
- w atrybucie zawierającym CSS: <span style="{{ foo }}"></span>
- w JavaScripcie: <script>var = {{ foo }}</script>
- w CSS: <style>body { content: {{ foo }}; }</style>
- w komentarzu: <!-- {{ foo }} -->

Naiwne systemy tylko mechanicznie konwertują znaki < > & ' " na encje HTML, co jest wprawdzie w większości przypadków użycia prawidłowym sposobem escapowania, ale zdecydowanie nie zawsze. Nie mogą więc wykryć ani zapobiec powstawaniu różnych luk bezpieczeństwa, jak pokażemy dalej.

Latte widzi szablon tak samo jak Ty. Rozumie HTML, XML, rozpoznaje znaczniki, atrybuty itp. A dzięki temu rozróżnia poszczególne konteksty i zgodnie z nimi oczyszcza dane. Oferuje w ten sposób naprawdę skuteczną ochronę przed krytyczną podatnością Cross-site Scripting.

░░░░░░░░░░░<span>{$foo}</span>
░░░░░░░░░░<span {$foo} ></span>
░░░░░░░░░░░░░░<span title='{$foo}'></span>
░░░░░░░░░░░░░░░░░░░░░░░░░░░<span title={$foo}></span>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<a href="{$foo}"></a>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<img onload="{$foo}">
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<span style="{$foo}"></span>
░░░░░░░░░░░░░░░░░<script>░░░░░░{$foo}</script>
░░░░░░░░░<style>░░░░░░░░░░░░░░░░{$foo}░░░</style>
░░░░░░░░░░░░░░░<!--░{$foo}░-->
- w tekście: <span>{$foo}</span>
- w tagu: <span {$foo} ></span>
- w atrybucie: <span title='{$foo}'></span>
- w atrybucie bez cudzysłowów: <span title={$foo}></span>
- w atrybucie zawierającym URL: <a href="{$foo}"></a>
- w atrybucie zawierającym JavaScript: <img onload="{$foo}">
- w atrybucie zawierającym CSS: <span style="{$foo}"></span>
- w JavaScripcie: <script>var = {$foo}</script>
- w CSS: <style>body { content: {$foo}; }</style>
- w komentarzu: <!-- {$foo} -->

Przykład na żywo

Po lewej stronie widać szablon w Latte, po prawej wygenerowany kod HTML. Kilka razy wyświetlana jest zmienna $text i za każdym razem w nieco innym kontekście. A więc i nieco inaczej escapowana. Kod szablonu możesz sam edytować, na przykład zmienić zawartość zmiennej itp. Spróbuj:

{* SPRÓBUJ ZMODYFIKOWAĆ TEN SZABLON *}
{var $text = "Rock'n'Roll"}
- <span>{$text}</span>
- <span title='{$text}'></span>
- <span title={$text}></span>
- <img onload="{$text}">
- <script>var = {$text}</script>
- <!-- {$text} -->
- <span>Rock'n'Roll</span>
- <span title='Rock&apos;n&apos;Roll'></span>
- <span title="Rock&apos;n&apos;Roll"></span>
- <img onload="&quot;Rock&apos;n&apos;Roll&quot;">
- <script>var = "Rock'n'Roll"</script>
- <!-- Rock'n'Roll -->

Czyż to nie wspaniałe! Latte wykonuje escapowanie kontekstowe automatycznie, więc programista:

  • nie musi myśleć ani wiedzieć, jak gdzie escapować
  • nie może się pomylić
  • nie może zapomnieć o escapowaniu

To nawet nie wszystkie konteksty, które Latte rozróżnia podczas wyświetlania i dla których dostosowuje oczyszczanie danych. Inne ciekawe przypadki omówimy teraz.

Jak zhakować naiwne systemy

Na kilku praktycznych przykładach pokażemy, jak ważne jest rozróżnianie kontekstów i dlaczego naiwne systemy szablonów nie zapewniają wystarczającej ochrony przed XSS, w przeciwieństwie do Latte. Jako przedstawiciela naiwnego systemu użyjemy w przykładach Twiga, ale to samo dotyczy innych systemów.

Podatność atrybutu

Spróbujemy wstrzyknąć do strony złośliwy kod za pomocą atrybutu HTML, jak pokazaliśmy powyżej. Miejmy szablon w Twigu renderujący obrazek:

<img src={{ imageFile }} alt={{ imageAlt }}>

Zwróć uwagę, że wokół wartości atrybutów nie ma cudzysłowów. Koder mógł o nich zapomnieć, co się po prostu zdarza. Na przykład w React kod pisze się w ten sposób, bez cudzysłowów, a koder, który zmienia języki, może łatwo zapomnieć o cudzysłowach.

Atakujący jako opis obrazka wstawia sprytnie skonstruowany ciąg foo onload=alert('Hacked!'). Już wiemy, że Twig nie może rozpoznać, czy zmienna jest wyświetlana w przepływie tekstu HTML, wewnątrz atrybutu, komentarza HTML itp., krótko mówiąc, nie rozróżnia kontekstów. I tylko mechanicznie konwertuje znaki < > & ' " na encje HTML. Więc wynikowy kod będzie wyglądał tak:

<img src=photo0145.webp alt=foo onload=alert(&#039;Hacked!&#039;)>

I powstała luka bezpieczeństwa!

Częścią strony stał się podrzucony atrybut onload, a przeglądarka natychmiast po pobraniu obrazka go uruchomi.

Teraz zobaczymy, jak z tym samym szablonem poradzi sobie Latte:

<img src={$imageFile} alt={$imageAlt}>

Latte widzi szablon tak samo jak Ty. W przeciwieństwie do Twiga rozumie HTML i wie, że zmienna jest wyświetlana jako wartość atrybutu, który nie jest w cudzysłowach. Dlatego je uzupełni. Kiedy atakujący wstawi ten sam opis, wynikowy kod będzie wyglądał tak:

<img src="photo0145.webp" alt="foo onload=alert(&apos;Hacked!&apos;)">

Latte skutecznie zapobiegło XSS.

Wyświetlanie zmiennej w JavaScript

Dzięki escapowaniu kontekstowemu możliwe jest całkowicie natywne używanie zmiennych PHP wewnątrz JavaScriptu.

<p onclick="alert({$movie})">{$movie}</p>

<script>var movie = {$movie};</script>

Jeśli zmienna $movie będzie zawierać ciąg 'Amarcord & 8 1/2', wygeneruje się następujące wyjście. Zwróć uwagę, że wewnątrz HTML użyje się innego escapowania niż wewnątrz JavaScriptu, a jeszcze innego w atrybucie onclick:

<p onclick="alert(&quot;Amarcord &amp; 8 1\/2&quot;)">Amarcord &amp; 8 1/2</p>

<script>var movie = "Amarcord & 8 1\/2";</script>

Sprawdzanie linków

Latte automatycznie sprawdza, czy zmienna użyta w atrybutach src lub href zawiera adres URL (tj. protokół HTTP) i zapobiega wyświetlaniu linków, które mogą stanowić zagrożenie bezpieczeństwa.

{var $link = 'javascript:attack()'}

<a href={$link}>kliknij</a>

Wyświetli:

<a href="">kliknij</a>

Kontrolę można wyłączyć za pomocą filtra nocheck.

Ograniczenia Latte

Latte nie jest całkowicie kompletną ochroną przed XSS dla całej aplikacji. Nie chcielibyśmy, abyś przy użyciu Latte przestał myśleć o bezpieczeństwie. Celem Latte jest zapewnienie, aby atakujący nie mógł zmodyfikować struktury strony, podrzucić elementów HTML lub atrybutów. Ale nie kontroluje poprawności treściowej wyświetlanych danych. Ani poprawności działania JavaScriptu. To już wykracza poza kompetencje systemu szablonów. Weryfikacja poprawności danych, zwłaszcza tych wprowadzonych przez użytkownika, a więc niezaufanych, jest ważnym zadaniem programisty.

wersja: 3.0