Latte jest synonimem bezpieczeństwa
Latte to jedyny system szablonów PHP z efektywną ochroną przed krytyczną luką Cross-site Scripting (XSS). Dzieje się tak dzięki tzw. ucieczce kontekstowej (context-sensitive escaping). Porozmawiajmy,
- czym jest luka XSS i dlaczego jest tak niebezpieczna
- co sprawia, że Latte tak skutecznie broni się przed XSS
- jak Twig, Blade, itp. mogą łatwo stworzyć dziurę bezpieczeństwa w szablonach
Cross-site Scripting (XSS)
Cross-site Scripting (w skrócie XSS) to jedna z najczęstszych luk w stronach internetowych, a przy tym bardzo niebezpieczna. Pozwala on atakującemu na umieszczenie złośliwego skryptu (zwanego malware) w obcej witrynie, który wykonuje się w przeglądarce niczego nie podejrzewającego użytkownika.
Co może zrobić taki skrypt? Może na przykład wysłać arbitralną treść ze skompromitowanej strony do atakującego, w tym wrażliwe dane wyświetlane po zalogowaniu. Może modyfikować stronę lub wykonywać inne żądania w imieniu użytkownika. Przykładowo, gdyby był to webmail, mógłby odczytać wrażliwe wiadomości, zmodyfikować wyświetlaną treść lub zmienić konfigurację, np. włączyć przekazywanie kopii wszystkich wiadomości na adres atakującego, aby uzyskać dostęp do przyszłych e-maili.
To również dlatego XSS znajduje się na szczycie listy najbardziej niebezpiecznych podatności. W przypadku znalezienia luki na stronie internetowej, należy ją jak najszybciej usunąć, aby zapobiec jej wykorzystaniu.
Jak powstaje ta podatność?
Luka występuje w miejscu, w którym generowana jest strona internetowa i wyprowadzane są zmienne. Wyobraź sobie, że tworzysz stronę wyszukiwania, a na początku będzie akapit z wyszukiwanym hasłem w postaci:
echo '<p>Výsledky vyhledávání pro <em>' . $search . '</em></p>';
Atakujący może wpisać dowolny ciąg znaków, w tym kod HTML, do pola wyszukiwania, a tym samym do zmiennej
$search
jako <script>alert("Hacked!")</script>
. Ponieważ wyjście nie jest traktowane w
żaden sposób, staje się częścią wyświetlanej strony:
<p>Výsledky vyhledávání pro <em><script>alert("Hacked!")</script></em></p>
Zamiast wypisać ciąg wyszukiwania, przeglądarka wykonuje JavaScript. I w ten sposób napastnik przejmuje stronę.
Można argumentować, że umieszczenie kodu w zmiennej rzeczywiście wykona JavaScript, ale tylko w przeglądarce atakującego. Jak to się dzieje, że trafia do ofiary? Z tej perspektywy możemy wyróżnić kilka rodzajów XSS. W naszym przykładzie wyszukiwania mówimy o refleksyjnym XSS. Tutaj musimy jeszcze naprowadzić ofiarę na kliknięcie w link, który w parametrze będzie zawierał złośliwy kod:
https://example.com/?search=<script>alert("Hacked!")</script>
Naprowadzenie użytkownika na link wymaga nieco inżynierii społecznej, ale nie jest to trudne. Użytkownicy klikają w linki,
czy to w mailach, czy w mediach 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 druga, znacznie groźniejsza forma ataku znana jako stored XSS lub persistent XSS, w której atakującemu udaje się przechowywać złośliwy kod na serwerze, tak aby był on automatycznie wstawiany na określone strony.
Przykładem tego są strony, na których użytkownicy zamieszczają komentarze. Napastnik wysyła post zawierający kod i jest on przechowywany na serwerze. Jeśli strona nie jest wystarczająco bezpieczna, będzie ona wtedy uruchamiana w przeglądarce każdego odwiedzającego.
Wydaje się, że istotą ataku jest wprowadzenie do strony ciągu znaków <script>
. W rzeczywistości istnieje wiele sposobów na osadzenie
JavaScript. Weźmy przykład osadzania za pomocą atrybutu HTML. Załóżmy galerię zdjęć, w której do zdjęć można
wstawić podpis, który jest drukowany w atrybucie alt
:
echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';
Atakujący musi tylko wstawić sprytnie skonstruowany ciąg " onload="alert('Hacked!')
jako napis, a jeśli
wyjście nie zostanie obsłużone, wynikowy kod będzie wyglądał tak:
<img src="photo0145.webp" alt="" onload="alert('Hacked!')">
Spofingowany atrybut onload
staje się teraz częścią strony. Przeglądarka wykonuje zawarty w nim kod, gdy
tylko obrazek zostanie pobrany. Zhakowany!
Jak bronić się przed XSS?
Wszelkie próby wykrycia ataku przy użyciu czarnej listy, np. ciągu blokad <script>
itp. są
niewystarczające. Podstawą skutecznej obrony jest konsekwentna sanityzacja wszystkich danych wyrzucanych wewnątrz
strony.
Przede wszystkim polega na zastąpieniu wszystkich znaków o specjalnym znaczeniu innymi odpowiadającymi im sekwencjami, co w
slangu oznacza escaping (pierwszy znak sekwencji nazywany jest znakiem escape, stąd nazwa). Na przykład w tekście HTML
znakiem o znaczeniu specjalnym jest <
, který když nemá být interpretován jako začátek tagu, musíme jej
nahradit vizuálně odpovídající sekvencí, tzv. HTML entitou <
. A przeglądarka drukuje mniejszość.
Bardzo ważne jest rozróżnienie kontekstu, w jakim dane są wyprowadzane. Ponieważ różne konteksty sanitują ciągi inaczej. W różnych kontekstach różne znaki mają specjalne znaczenie. Na przykład ucieczka różni się w tekście HTML, w atrybutach HTML, wewnątrz niektórych elementów specjalnych itp. Za chwilę omówimy to szczegółowo.
Najlepiej jest wykonać escaping bezpośrednio podczas wypisywania ciągu na stronie, zapewniając, że jest on rzeczywiście wykonywany i wykonywany tylko raz. Najlepiej, jeśli obróbka jest obsługiwana automatycznie bezpośrednio przez system szablonów. Ponieważ jeśli leczenie nie jest wykonywane automatycznie, programista może o nim zapomnieć. A jedno przeoczenie oznacza, że strona jest narażona na niebezpieczeństwo.
Jednak XSS nie wpływa tylko na wyprowadzanie danych w szablonach, ale także na inne części aplikacji, które muszą
prawidłowo obsługiwać niezaufane dane. Na przykład JavaScript w twojej aplikacji nie może używać innerHTML
,
ale tylko innerText
lub textContent
w połączeniu z nimi. Należy zachować szczególną ostrożność
w przypadku funkcji oceniających ciągi znaków, takich jak JavaScript, który jest eval()
, ale także
setTimeout()
, lub używając funkcji setAttribute()
z atrybutami zdarzeń, takimi jak
onload
, itp. Ale to wykracza poza zakres objęty szablonami.
idealna obrona 3-punktowa:
- Rozpoznać kontekst, w którym dane są wyprowadzane
- oczyszcza dane zgodnie z zasadami tego kontekstu (tj. “context sensitive”)
- robi to automatycznie
Ucieczka kontekstowa
Co dokładnie oznacza słowo kontekst? Jest to miejsce w dokumencie z własnymi zasadami obsługi danych wyjściowych. Zależy on od rodzaju dokumentu (HTML, XML, CSS, JavaScript, zwykły tekst, …) i może się różnić w określonych częściach dokumentu. Na przykład w dokumencie HTML istnieje wiele takich miejsc (kontekstów), w których obowiązują bardzo różne zasady. Możesz być zaskoczony, jak wiele ich jest. Oto pierwsze cztery:
<p>#text</p>
<img src="#atribut">
<textarea>#rawtext</textarea>
<!-- #komentář -->
Początkowym i podstawowym kontekstem strony HTML jest tekst HTML. Jakie są tu zasady? Szczególne znaczenie mają znaki
<
a &
, które reprezentują początek znacznika lub encji, dlatego musimy uciec, zastępując je
encją HTML (<
za <
&
za &
).
Drugim najczęstszym kontekstem jest wartość atrybutu HTML. Od tekstu różni się tym, że specjalne znaczenie ma tu
cudzysłów "
nebo '
, który ogranicza atrybut. To powinno być napisane z podmiotem, aby nie było
postrzegane jako koniec atrybutu. I odwrotnie, znak <
może być bezpiecznie użyty w atrybucie, ponieważ
nie ma tu specjalnego znaczenia; nie może być postrzegany jako początek znacznika lub komentarza. Ale uwaga, w HTML można
pisać wartości atrybutów bez cudzysłowów, w takim przypadku cały szereg znaków ma specjalne znaczenie, więc jest to
kolejny oddzielny kontekst.
Może Cię to zaskoczy, ale wewnątrz elementów obowiązują specjalne zasady <textarea>
a
<title>
, gdzie zastosowano znak <
nemusí (ale může) escapovat, pokud za ním nenásleduje
/
Ale to raczej perełka.
Ciekawa rzecz znajduje się wewnątrz komentarzy HTML. Tutaj encje HTML nie są używane do ucieczki. Nie ma nawet specyfikacji, która określa, jak uciekać w komentarzach. Trzeba tylko przestrzegać nieco ciekawych zasad i unikać w nich pewnych kombinacji znaków.
Konteksty mogą być również warstwowe, co ma miejsce, gdy osadzamy JavaScript lub CSS w HTML-u. Można to zrobić na dwa różne sposoby, element i atrybut:
<script>#js-element</script>
<img onclick="#js-atribut">
<style>#css-element</style>
<p style="#css-atribut"></p>
Dwie ścieżki i dwa różne sposoby ucieczki od danych. Wewnątrz elementu <script>
a
<style>
tak jak w przypadku komentarzy HTML, ucieczka przy użyciu encji HTML nie jest wykonywana. Jedyną
zasadą, której należy przestrzegać podczas ucieczki od danych wewnątrz tych elementów jest to, że tekst nie może zawierać
sekwencji </script
lub </style
.
Natomiast atrybuty style
i on***
są escape'owane przy użyciu encji HTML.
I, oczywiście, zasady ucieczki tych języków mają zastosowanie wewnątrz osadzonego JavaScript lub CSS. Tak więc ciąg w
atrybucie takim jak onload
jest najpierw escaped zgodnie z regułami JS, a następnie zgodnie z regułami
atrybutów HTML.
Ugh… Jak widać, HTML jest bardzo złożonym dokumentem z warstwami kontekstów, a nie wiedząc dokładnie, gdzie wyprowadzam dane (tj. W jakim kontekście), nie ma mowy o tym, jak zrobić to dobrze.
Chcesz przykład?
Niech będzie to ciąg Rock'n'Roll
.
Jeżeli wyprowadzasz go w postaci tekstu HTML, to w tym przypadku nie ma potrzeby dokonywania żadnych podstawień, ponieważ łańcuch nie zawiera żadnego znaku o specjalnym znaczeniu. Sytuacja wygląda inaczej, jeśli napiszemy go wewnątrz atrybutu HTML ujętego w pojedyncze cudzysłowy. W tym przypadku musisz uciec od cytatów do encji HTML:
<div title='Rock'n'Roll'></div>
To było łatwe. Znacznie ciekawsza sytuacja występuje w przypadku kontekstów warstwowych, na przykład jeśli ciąg jest częścią JavaScript.
Najpierw więc wypisujemy go do samego JavaScriptu. Czyli zawijamy go w cudzysłów, jednocześnie uciekając od zawartych w
nim cudzysłowów za pomocą znaku \
:
'Rock\'n\'Roll'
Możemy dodać wywołanie funkcji, aby kod coś zrobił:
alert('Rock\'n\'Roll');
Jeśli wstawimy ten kod do dokumentu HTML używając <script>
, nie jest potrzebna dalsza edycja, ponieważ
zabroniona sekwencja </script
nie występuje:
<script> alert('Rock\'n\'Roll'); </script>
Jeśli jednak chcemy wstawić go do atrybutu HTML, nadal musimy uciec od cudzysłowów do encji HTML:
<div onclick='alert('Rock\'n\'Roll')'></div>
Zagnieżdżony kontekst nie musi być JS ani CSS. Parametry w adresach URL są escape'owane przez konwersję znaków
specjalnych na sekwencje zaczynające się od %
. Przykład:
https://example.org/?a=Jazz&b=Rock%27n%27Roll
A kiedy wyprowadzamy ten ciąg w atrybucie, nadal stosujemy escaping zgodnie z tym kontekstem i zastępujemy
&
za &
:
<a href="https://example.org/?a=Jazz&b=Rock%27n%27Roll">
Jeśli przeczytałeś to do tej pory, gratuluję, to było wyczerpujące. Teraz masz dobre pojęcie o tym, czym są konteksty i ucieczka. I nie musisz się martwić, że będzie to skomplikowane. Latte robi to za Ciebie automatycznie.
Latte a systemy naiwne
Pokazaliśmy, jak prawidłowo stosować escaping w dokumencie HTML i jak kluczowa jest znajomość kontekstu, czyli miejsca, w którym wyprowadzamy dane. Innymi słowy, jak działa ucieczka kontekstowa. Chociaż jest to warunek wstępny dla funkcjonalnej obrony przed XSS, Latte jest jedynym systemem szablonów dla PHP, który to robi.
Jak to możliwe, skoro wszystkie dzisiejsze systemy twierdzą, że mają automatyczną ucieczkę? Automatyczne uciekanie bez znajomości kontekstu to bzdura, która tworzy fałszywe poczucie bezpieczeństwa.
Systemy szablonujące takie jak Twig, Laravel Blade i inne nie widzą w szablonie żadnej struktury HTML. Dlatego nie widzą też kontekstów. W porównaniu z Latte są ślepi i naiwni. Obsługują tylko niestandardowe tagi, wszystko inne jest dla nich nieistotnym strumieniem znaków:
░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░
░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░
- v textu: <span>{{ text }}</span>
- v tagu: <span {{ text }} ></span>
- v atributu: <span title='{{ text }}'></span>
- v atributu bez uvozovek: <span title={{ text }}></span>
- v atributu obsahujícím URL: <a href="{{ text }}"></a>
- v atributu obsahujícím JavaScript: <img onload="{{ text }}">
- v atributu obsahujícím CSS: <span style="{{ text }}"></span>
- v JavaScriptu: <script>var = {{ text }}</script>
- v CSS: <style>body { content: {{ text }}; }</style>
- v komentáři: <!-- {{ text }} -->
Systemy naiwne po prostu mechanicznie konwertują znaki < > & ' "
na encje HTML, co jest poprawną
metodą ucieczki w większości przypadków użycia, ale daleko od zawsze. Nie mogą więc wykryć ani zapobiec różnym lukom w
zabezpieczeniach, co pokażemy poniżej.
Latte widzi szablon tak samo jak ty. Rozumie HTML, XML, rozpoznaje tagi, atrybuty itp. A dzięki temu rozróżnia konteksty i odpowiednio traktuje dane. Oferuje więc naprawdę skuteczną ochronę przed krytyczną podatnością Cross-site Scripting.
Demonstracja na żywo
Po lewej stronie widać szablon w Latte, po prawej wygenerowany kod HTML. Zmienna $text
jest wyświetlana
kilkakrotnie, za każdym razem w nieco innym kontekście. I w ten sposób uciekł trochę inaczej. Możesz samodzielnie edytować
kod szablonu, np. zmienić zawartość zmiennej itp. Spróbuj:
Czyż nie jest to wspaniałe! Latte robi kontekstowe ucieczki automatycznie, więc programista:
- nie musi myśleć ani wiedzieć, jak uciec, gdzie
- nie można się pomylić
- nie można zapomnieć o ucieczce
To nawet nie są wszystkie konteksty, które Latte rozróżnia przy wyprowadzaniu i dla których dostosowuje obróbkę danych. Teraz przejdziemy przez ciekawsze przypadki.
Jak włamać się do systemów naiwnych
Na kilku praktycznych przykładach pokażemy jak ważne jest rozróżnianie kontekstu i dlaczego naiwne systemy templatek nie zapewniają wystarczającej ochrony przed XSS, w przeciwieństwie do Latte. W przykładach użyjemy Twiga jako przedstawiciela systemu naiwnego, ale to samo dotyczy innych systemów.
Podatność atrybutów
Spróbujemy wstrzyknąć złośliwy kod do strony za pomocą atrybutu HTML, jak pokazaliśmy powyżej. Miejmy na uwadze, że szablon w Twigu renderuje obraz:
<img src={{ imageFile }} alt={{ imageAlt }}>
Zauważ, że wokół wartości atrybutów nie ma cudzysłowów. Koder mógł o nich zapomnieć, co po prostu się zdarza. Na przykład w React kod jest napisany tak, bez cytatów, a koder, który zmienia języki, może łatwo zapomnieć o cytatach.
Atakujący wstawiłby sprytnie skonstruowany ciąg znaków foo onload=alert('Hacked!')
jako podpis obrazka. Wiemy
już, że Twig nie potrafi określić, czy zmienna jest wyprowadzana w strumieniu tekstu HTML, wewnątrz atrybutu, wewnątrz
komentarza HTML itd. I po prostu mechanicznie konwertuje znaki < > & ' "
na jednostki HTML. Tak więc
wynikowy kod będzie wyglądał tak:
<img src=photo0145.webp alt=foo onload=alert('Hacked!')>
Powstała dziura w zabezpieczeniach!
Fałszywy atrybut onload
stał się częścią strony i przeglądarka uruchamia go natychmiast po pobraniu
obrazu.
Teraz zobaczmy, jak Latte radzi sobie z tym samym szablonem:
<img src={$imageFile} alt={$imageAlt}>
Latte widzi szablon tak samo jak ty. W przeciwieństwie do Twig, rozumie HTML i wie, że zmienna jest drukowana jako wartość atrybutu, która nie jest w cudzysłowie. Dlatego też dodaje je. Gdy atakujący wstawi tę samą etykietę, wynikowy kod będzie wyglądał tak:
<img src="photo0145.webp" alt="foo onload=alert('Hacked!')">
Latte skutecznie zapobiegła XSS.
Wypisywanie zmiennej w JavaScript
Dzięki ucieczce kontekstowej, możliwe jest używanie zmiennych PHP natywnie wewnątrz JavaScript.
<p onclick="alert({$movie})">{$movie}</p>
<script>var movie = {$movie};</script>
Jeśli zmienna $movie
zawiera ciąg 'Amarcord & 8 1/2'
, to zostanie wygenerowane następujące
wyjście. Zauważ, że wewnątrz HTML używane jest inne escaping niż wewnątrz JavaScript, a nawet inne w atrybucie
onclick
:
<p onclick="alert("Amarcord & 8 1\/2")">Amarcord & 8 1/2</p>
<script>var movie = "Amarcord & 8 1\/2";</script>
Kontrola połączeń
Latte automatycznie sprawdza, czy zmienna użyta w atrybutach src
lub href
zawiera adres URL strony
internetowej (tj. protokół HTTP) i zapobiega wypisywaniu linków, które mogą stanowić zagrożenie dla bezpieczeństwa.
{var $link = 'javascript:attack()'}
<a href={$link}>click here</a>
Wydruki:
<a href="">klikni</a>
Sprawdzanie można wyłączyć za pomocą filtra nocheck.
Limity na latte
Latte nie jest kompletną ochroną XSS dla całej aplikacji. Nie chcielibyśmy, abyś przestał myśleć o bezpieczeństwie podczas korzystania z Latte. Celem Latte jest zapewnienie, że atakujący nie może zmienić struktury strony, ani sfałszować elementów lub atrybutów HTML. Ale nie sprawdza poprawności merytorycznej danych wyjściowych. Albo poprawność zachowania JavaScript. To wykracza poza zakres systemu szablonowania. Weryfikacja poprawności danych, zwłaszcza wprowadzanych przez użytkownika, a więc niezaufanych, jest ważnym zadaniem dla programisty.