Latte to synonim bezpieczeństwa

Latte jest jedynym systemem szablonów dla PHP ze skuteczną ochroną przed krytyczną podatnością Cross-site Scripting (XSS). Zawdzięcza to escapowaniu świadomemu kontekstu. Omówimy:

  • na czym polega podatność XSS i dlaczego jest tak niebezpieczna
  • co sprawia, że Latte tak skutecznie broni przed XSS
  • jak łatwo można stworzyć luki bezpieczeństwa w szablonach Twig, Blade i podobnych

Cross-site Scripting (XSS)

Cross-site Scripting (w skrócie XSS) to jedna z najczęstszych i najniebezpieczniejszych podatności stron internetowych. Pozwala napastnikowi wstrzyknąć na cudzą stronę złośliwy skrypt (malware), który następnie uruchomi się w przeglądarce niczego nieświadomego użytkownika.

Co taki skrypt może zrobić? Może na przykład wysłać napastnikowi dowolną treść z zaatakowanej strony, w tym wrażliwe dane wyświetlane po zalogowaniu. Może zmodyfikować stronę albo wykonać w imieniu użytkownika inne żądania. Gdyby chodziło na przykład o pocztę webową, mógłby odczytać wrażliwe wiadomości, zmienić wyświetlaną treść albo przestawić ustawienia, np. włączyć przekazywanie kopii wszystkich wiadomości na adres napastnika, aby uzyskać dostęp także do przyszłych e-maili.

Dlatego XSS niezmiennie plasuje się wśród najniebezpieczniejszych podatności. Jeśli pojawi się na stronie, trzeba ją usunąć jak najszybciej, aby zapobiec wykorzystaniu.

Jak powstaje ta podatność?

Błąd pojawia się tam, gdzie generowana jest strona i wypisywane są zmienne. Wyobraź sobie, że tworzysz stronę wyszukiwania, na której początku znajduje się akapit z szukanym wyrażeniem, o tak:

echo '<p>Search results for <em>' . $search . '</em></p>';

Napastnik może wpisać w pole wyszukiwania dowolny łańcuch, a więc również do zmiennej $search, w tym kod HTML w rodzaju <script>alert("Hacked!")</script>. Ponieważ wynik nie jest w żaden sposób oczyszczany, staje się częścią wyświetlanej strony:

<p>Search results for <em><script>alert("Hacked!")</script></em></p>

Zamiast wyświetlić szukany łańcuch, przeglądarka wykona JavaScript. I tym samym napastnik przejmuje kontrolę nad stroną.

Możesz zaoponować, że wstawienie kodu do zmiennej wprawdzie wykona JavaScript, ale tylko w przeglądarce napastnika. Jak dotrze do ofiary? Z tego punktu widzenia rozróżniamy kilka rodzajów XSS. W naszym przykładzie z wyszukiwaniem mówimy o reflected XSS. Tutaj trzeba jeszcze nakłonić ofiarę, aby kliknęła odnośnik zawierający złośliwy kod w parametrze:

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

Nakłonienie użytkownika do kliknięcia odnośnika wymaga pewnej socjotechniki, ale nie jest to szczególnie skomplikowane. Użytkownicy klikają w odnośniki, czy to w e-mailach, czy w mediach społecznościowych, bez większego namysłu. To, że adres zawiera coś podejrzanego, da się zamaskować skracaczem URL; użytkownik zobaczy wtedy tylko bit.ly/xxx.

Istnieje jednak druga, o wiele niebezpieczniejsza forma ataku, znana jako stored XSS albo persistent XSS, w której napastnikowi udaje się zapisać złośliwy kod na serwerze, tak że jest on automatycznie wstawiany na pewne strony.

Przykładem są strony, na których użytkownicy piszą komentarze. Napastnik wysyła wpis zawierający kod, a ten zostaje zapisany na serwerze. Jeśli strony nie są dostatecznie zabezpieczone, kod uruchomi się potem w przeglądarce każdego odwiedzającego.

Mogłoby się wydawać, że sedno ataku polega na przemyceniu na stronę łańcucha <script>. W rzeczywistości sposobów na wstawienie JavaScriptu jest wiele. Pokażmy przykład z użyciem atrybutu HTML. Weźmy galerię zdjęć, w której do obrazków można dodawać podpisy wyświetlane w atrybucie alt:

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

Napastnikowi wystarczy wstawić jako podpis sprytnie ułożony łańcuch " onload="alert('Hacked!'), a jeśli wynik nie zostanie oczyszczony, powstały kod będzie wyglądał tak:

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

Podstawiony atrybut onload staje się teraz częścią strony. Przeglądarka wykona zawarty w nim kod natychmiast po pobraniu obrazka. Hacked!

Jak bronić się przed XSS?

Wszelkie próby wykrywania ataków przy użyciu czarnych list, na przykład blokowania łańcucha <script>, są niewystarczające. Podstawą działającej obrony jest konsekwentne oczyszczanie wszystkich danych wypisywanych na stronie.

Chodzi przede wszystkim o zastąpienie wszystkich znaków o specjalnym znaczeniu odpowiadającymi im sekwencjami, potocznie nazywane escapowaniem (pierwszy znak sekwencji nazywa się znakiem escape, stąd nazwa). Na przykład w tekście HTML znak < ma specjalne znaczenie; jeśli nie ma być interpretowany jako początek tagu, musimy zastąpić go wizualnie odpowiadającą sekwencją, czyli encją HTML &lt;. Przeglądarka wyświetli wtedy znak mniejszości.

Kluczowe jest rozróżnienie kontekstu, w którym dane są wypisywane. Bo w różnych kontekstach łańcuchy oczyszcza się różnie. W różnych kontekstach specjalne znaczenie mają różne znaki. Na przykład escapowanie różni się w tekście HTML, w atrybutach HTML, wewnątrz niektórych elementów specjalnych itd. Omówimy to szczegółowo za chwilę.

Oczyszczanie najlepiej wykonywać dokładnie w chwili wypisywania łańcucha na stronę, co zapewnia, że rzeczywiście zostanie wykonane i to dokładnie raz. Najlepiej, gdy oczyszczaniem zajmuje się automatycznie sam system szablonów. Bo jeśli oczyszczanie nie jest automatyczne, programista może o nim zapomnieć. A jedno przeoczenie oznacza, że strona jest podatna.

XSS nie dotyczy jednak tylko wypisywania danych w szablonach, ale też innych części aplikacji, które muszą poprawnie obchodzić się z niezaufanymi danymi. Na przykład JavaScript w Twojej aplikacji musi w połączeniu z niezaufanymi danymi używać innerText albo textContent, a nie innerHTML. Szczególną uwagę trzeba poświęcić funkcjom, które wykonują łańcuchy jako JavaScript, takim jak eval(), ale też setTimeout(), albo użyciu setAttribute() z atrybutami zdarzeń w rodzaju onload itd. To wykracza jednak poza obszar, którym zajmują się szablony.

Idealna obrona w 3 punktach:

  1. Rozpoznaje kontekst, w którym dane są wypisywane.
  2. Oczyszcza dane według reguł tego kontekstu (czyli jest “context-aware”).
  3. Robi to automatycznie.

Escapowanie świadome kontekstu

Co dokładnie kryje się pod słowem kontekst? To miejsce w dokumencie z własnymi regułami obchodzenia się z wypisywanymi danymi. Zależy od typu dokumentu (HTML, XML, CSS, JavaScript, zwykły tekst, …) i może różnić się w poszczególnych częściach. Na przykład w dokumencie HTML jest wiele miejsc (kontekstów), w których obowiązują bardzo różne reguły. Możesz się zdziwić, jak wiele ich jest. Oto pierwsze cztery:

<p>#text</p>
<img src="#attribute">
<textarea>#rawtext</textarea>
<!-- #comment -->

Domyślnym i podstawowym kontekstem strony HTML jest tekst HTML. Jakie reguły tu obowiązują? Specjalne znaczenie mają znaki < i &, oznaczające początek tagu albo encji, więc musimy je escapować, zastępując encjami HTML (< staje się &lt;, & staje się &amp;).

Drugim najczęstszym kontekstem jest wartość atrybutu HTML. Różni się od tekstu tym, że specjalne znaczenie ma tu cudzysłów " albo ', który ogranicza atrybut. Trzeba go zapisać jako encję, aby nie został zinterpretowany jako koniec atrybutu. Odwrotnie, znak < można w atrybucie bezpiecznie użyć, bo nie ma tam specjalnego znaczenia; nie da się go zinterpretować jako początek tagu ani komentarza. Ale uwaga, w HTML wartości atrybutów można zapisywać także bez cudzysłowów, a wtedy specjalne znaczenie ma cały szereg znaków, co czyni z tego kolejny, osobny kontekst.

Może Cię to zdziwić, ale specjalne reguły obowiązują wewnątrz elementów <textarea> i <title>, gdzie znaku < nie trzeba (choć można) escapować, o ile nie następuje po nim /. To jednak raczej drobiazg.

Ciekawie robi się wewnątrz komentarzy HTML. Tutaj do escapowania nie używa się encji HTML. Właściwie żadna specyfikacja nie mówi, jak escapować w komentarzach. Trzeba tylko trzymać się nieco osobliwych reguł i unikać w nich pewnych kombinacji znaków.

Konteksty mogą się też nakładać, co dzieje się wtedy, gdy osadzamy w HTML JavaScript albo CSS. Można to zrobić na dwa różne sposoby, elementem albo atrybutem:

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

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

Dwie drogi i dwa różne sposoby escapowania danych. Wewnątrz elementów <script> i <style>, podobnie jak w komentarzach HTML, escapowania encjami HTML się nie stosuje. Przy wypisywaniu danych wewnątrz tych elementów trzeba trzymać się tylko jednej reguły: tekst nie może zawierać odpowiednio sekwencji </script ani </style.

Odwrotnie, w atrybutach style i on*** escapuje się encjami HTML.

I oczywiście wewnątrz zagnieżdżonego JavaScriptu czy CSS obowiązują reguły escapowania tych języków. Łańcuch w atrybucie takim jak onload jest więc escapowany najpierw według reguł JS, a potem według reguł atrybutów HTML.

Uff… Jak widzisz, HTML to bardzo złożony dokument, w którym konteksty nakładają się na siebie, i bez uświadomienia sobie, gdzie dokładnie wypisujesz dane (czyli w jakim kontekście), nie da się powiedzieć, jak zrobić to poprawnie.

Chcesz przykład?

Weźmy łańcuch Rock'n'Roll.

Jeśli wypiszesz go w tekście HTML, w tym konkretnym przypadku żadna zamiana nie jest potrzebna, bo łańcuch nie zawiera znaków o specjalnym znaczeniu. Sytuacja zmienia się, gdy wypiszesz go wewnątrz atrybutu HTML ujętego w pojedyncze cudzysłowy. Wtedy trzeba escapować cudzysłowy do encji HTML:

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

To było proste. O wiele ciekawsza sytuacja pojawia się, gdy konteksty się nakładają, na przykład gdy łańcuch jest częścią JavaScriptu.

Najpierw wypiszmy go w samym JavaScripcie. Czyli ujmiemy go w cudzysłowy i jednocześnie escapujemy zawarte w nim cudzysłowy znakiem \:

'Rock\'n\'Roll'

Możemy jeszcze dodać wywołanie funkcji, aby kod coś robił:

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

Jeśli wstawimy ten kod do dokumentu HTML za pomocą <script>, żadna dalsza modyfikacja nie jest potrzebna, bo nie zawiera on zabronionej sekwencji </script:

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

Gdybyśmy jednak chcieli wstawić go do atrybutu HTML, musimy jeszcze escapować cudzysłowy do encji HTML:

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

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

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

A gdy wypiszemy ten łańcuch w atrybucie, zastosujemy jeszcze escapowanie zgodne z tym kontekstem i zamienimy & na &amp;:

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

Jeśli doczytałeś aż tutaj, gratulacje, było tego sporo. Teraz dobrze rozumiesz, czym są konteksty i escapowanie. I nie musisz się martwić, że to skomplikowane. Latte robi to za Ciebie automatycznie.

Latte kontra systemy naiwne

Pokazaliśmy, jak poprawnie escapować w dokumencie HTML i jak kluczowa jest znajomość kontekstu, czyli miejsca, w którym wypisujemy dane. Innymi słowy, jak działa escapowanie świadome kontekstu. Choć jest to niezbędny warunek działającej obrony przed XSS, Latte jest jedynym systemem szablonów dla PHP, który to potrafi.

Jak to możliwe, skoro dziś wszystkie systemy twierdzą, że mają automatyczne escapowanie? Automatyczne escapowanie bez znajomości kontekstu to trochę mydlenie oczu, które 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 z Latte są ślepe i naiwne. Przetwarzają tylko własne tagi; cała reszta jest dla nich nic nieznaczącym 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 }} -->

Systemy naiwne mechanicznie zamieniają znaki < > & ' " na encje HTML, co jest wprawdzie poprawną metodą escapowania w większości zastosowań, ale daleko jej do wystarczającej w każdym przypadku. Nie potrafią więc wykryć ani zapobiec powstawaniu różnych luk bezpieczeństwa, co pokażemy poniżej.

Latte widzi szablon dokładnie tak jak Ty. Rozumie HTML, XML, rozpoznaje tagi, atrybuty itd. I dzięki temu rozróżnia poszczególne konteksty i odpowiednio obchodzi się z danymi. Oferuje więc 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} -->

Demonstracja na żywo

Po lewej widzisz szablon w Latte, po prawej wygenerowany kod HTML. Zmienna $text jest wypisywana kilkakrotnie, za każdym razem w nieco innym kontekście. I tym samym nieco inaczej escapowana. Kod szablonu możesz sam edytować, na przykład zmienić zawartość zmiennej itd. Wypróbuj:

{* SPRÓBUJ EDYTOWAĆ 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 świadome kontekstu automatycznie, więc programista:

  • nie musi zastanawiać się ani wiedzieć, jak i gdzie escapować
  • nie może się pomylić
  • nie może zapomnieć o escapowaniu

To nawet nie są wszystkie konteksty, które Latte rozróżnia przy wypisywaniu i do których dostosowuje obchodzenie się z danymi. Przejdziemy teraz przez kolejne ciekawe przypadki.

Jak zhakować systemy naiwne

Na kilku praktycznych przykładach pokażemy, jak ważne jest rozróżnianie kontekstów i dlaczego naiwne systemy szablonów, w odróżnieniu od Latte, nie dają wystarczającej ochrony przed XSS. W przykładach użyjemy Twiga jako przedstawiciela systemu naiwnego, ale to samo dotyczy innych systemów.

Podatność w atrybucie

Spróbujmy wstrzyknąć na stronę złośliwy kod za pomocą atrybutu HTML, tak jak pokazaliśmy powyżej. Weźmy 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 Reakcie kod pisze się właśnie tak, bez cudzysłowów, i koder przeskakujący między językami łatwo o nie zapomni.

Napastnik wstawia jako podpis obrazka sprytnie ułożony łańcuch foo onload=alert('Hacked!'). Wiemy już, że Twig nie potrafi ustalić, czy zmienna jest wypisywana w toku tekstu HTML, wewnątrz atrybutu, w komentarzu HTML itd.; krótko mówiąc, nie rozróżnia kontekstów. I tylko mechanicznie zamienia znaki < > & ' " na encje HTML. Powstały kod będzie więc wyglądał tak:

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

Powstała luka bezpieczeństwa!

Podstawiony atrybut onload stał się częścią strony, a przeglądarka wykona go zaraz po pobraniu obrazka.

Zobaczmy teraz, jak z tym samym szablonem poradzi sobie Latte:

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

Latte widzi szablon tak samo jak Ty. W odróżnieniu od Twiga rozumie HTML i wie, że zmienna jest wypisywana jako wartość atrybutu, który nie jest ujęty w cudzysłowy. Dlatego je dodaje. Gdy napastnik wstawi ten sam podpis, powstały kod będzie wyglądał tak:

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

Latte skutecznie zapobiegło XSS.

Wypisywanie zmiennej w JavaScripcie

Dzięki escapowaniu świadomemu kontekstu można natywnie używać zmiennych PHP wewnątrz JavaScriptu.

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

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

Jeśli zmienna $movie zawiera łańcuch 'Amarcord & 8 1/2', wygeneruje się poniższy wynik. Zwróć uwagę na inne escapowanie użyte wewnątrz HTML w porównaniu z wnętrzem JavaScriptu, a jeszcze inne 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>

Kontrola odnośników

Latte automatycznie sprawdza, czy zmienna użyta w atrybutach z adresem URL, takich jak src, href, action, formaction czy data w elemencie <object>, zawiera bezpieczny URL. Dopuszcza powszechne protokoły (http, https, ftp, mailto, tel, sms) oraz adresy względne, a blokuje potencjalnie niebezpieczne, takie jak javascript:.

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

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

Wypisze:

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

Kontrolę można wyłączyć filtrem nocheck.

Frameworki po stronie klienta

Frameworki takie jak Vue czy Angular traktują część strony jako własny szablon i interpretują wewnątrz niego podwójne klamry {{ ... }} jako wyrażenie. Jeśli napastnikowi uda się przemycić taką sekwencję do wypisywanej zmiennej, framework obliczy ją w przeglądarce po załadowaniu strony, mimo że po stronie serwera do żadnego XSS nie doszło.

Latte temu zapobiega. Przy wypisywaniu do tekstu HTML zamienia parę {{ na {<!-- -->{; pusty komentarz jest dla czytelnika niewidoczny, a jednocześnie uniemożliwia frameworkowi rozpoznanie własnego tagu. Każde inne wystąpienie znaku { jest zastępowane encją &#123; (wewnątrz wartości atrybutów escapowany jest w ten sposób każdy {).

{var $query = 'Hi {{ constructor.constructor("alert(1)")() }}'}

<h1>Szukaj: {$query}</h1>

Wypisze:

<h1>Szukaj: Hi {<!-- -->{ constructor.constructor("alert(1)")() }}</h1>

Ochrona działa automatycznie i nie wymaga żadnej konfiguracji.

Granice Latte

Latte nie jest kompletną ochroną przed XSS dla całej aplikacji. Byłoby nam przykro, gdybyś przy używaniu Latte przestał myśleć o bezpieczeństwie. Celem Latte jest zapewnienie, że napastnik nie zmieni struktury strony ani nie podstawi elementów czy atrybutów HTML. Nie sprawdza jednak poprawności treści wypisywanych danych. Ani poprawności zachowania JavaScriptu. To wykracza poza kompetencje systemu szablonów. Weryfikacja poprawności danych, zwłaszcza wprowadzanych przez użytkownika, a więc niezaufanych, to ważne zadanie programisty.

Chcesz sprawdzić swoją wiedzę o escapowaniu w poszczególnych kontekstach? Wypróbuj quiz o podatności XSS.

wersja: 3.x