Latte je synonymum bezpečnosti
Latte je jediný šablonovací systém pro PHP, který poskytuje účinnou ochranu proti kritické zranitelnosti Cross-site Scripting (XSS). Tuto ochranu zajišťuje pomocí tzv. kontextově sensitivního escapování. V tomto článku se dozvíte:
- Princip zranitelnosti XSS a proč je tak nebezpečná
- Proč je Latte v obraně před XSS tak efektivní
- Jak lze v šablonách jiných systémů jako Twig či Blade snadno vytvořit bezpečnostní mezeru
Cross-site Scripting (XSS)
Cross-site Scripting, zkráceně XSS, patří mezi nejčastější a zároveň velmi nebezpečné zranitelnosti webových stránek. Umožňuje útočníkovi vložit do cizí webové stránky škodlivý skript (malware), který se následně spustí v prohlížeči nic netušícího uživatele.
Takový škodlivý skript může napáchat značné škody. Může například:
- Odeslat útočníkovi jakýkoli obsah z napadené stránky, včetně citlivých údajů zobrazených po přihlášení
- Modifikovat obsah stránky
- Provádět akce jménem přihlášeného uživatele
Představte si například napadení webmailové služby. Útočník by mohl číst citlivé zprávy, měnit zobrazovaný obsah nebo upravovat nastavení, třeba zapnout přeposílání kopií všech zpráv na svou adresu, čímž by získal přístup i k budoucí komunikaci.
Proto se XSS pravidelně objevuje na předních příčkách žebříčků nejnebezpečnějších zranitelností. Pokud se na webové stránce objeví tato zranitelnost, je klíčové ji co nejrychleji odstranit, aby se zabránilo potenciálnímu zneužití.
Jak zranitelnost vzniká?
XSS zranitelnost vzniká v místě, kde se generuje webová stránka a vypisují se proměnné. Představte si například stránku s vyhledáváním, kde se zobrazuje hledaný výraz:
echo '<p>Výsledky vyhledávání pro <em>' . $search . '</em></p>';
Útočník může do vyhledávacího pole a následně do proměnné $search
vložit libovolný řetězec,
včetně HTML kódu jako <script>alert("Hacked!")</script>
. Pokud není výstup nijak ošetřen, stane se
součástí zobrazené stránky:
<p>Výsledky vyhledávání pro <em><script>alert("Hacked!")</script></em></p>
Prohlížeč místo zobrazení hledaného výrazu spustí vložený JavaScript, čímž útočník získává kontrolu nad stránkou.
Existuje několik typů XSS útoků. V našem příkladu s vyhledáváním jde o tzv. reflected XSS. Zde útočník potřebuje přimět oběť kliknout na odkaz obsahující škodlivý kód v parametru:
https://example.com/?search=<script>alert("Hacked!")</script>
I když to vyžaduje určité sociální inženýrství, není to nic složitého. Uživatelé často klikají na odkazy v emailech nebo na sociálních sítích bez většího rozmyslu. Podezřelou část adresy lze navíc zamaskovat pomocí zkracovače URL.
Ještě nebezpečnější formou je stored XSS nebo persistent XSS, kdy se útočníkovi podaří uložit škodlivý kód přímo na server. Typickým příkladem jsou stránky s uživatelskými komentáři. Útočník odešle příspěvek obsahující škodlivý kód, který se uloží na server. Pokud nejsou stránky dostatečně zabezpečené, bude se tento kód spouštět v prohlížeči každého návštěvníka.
Mohlo by se zdát, že jádro útoku spočívá ve vložení řetězce <script>
do stránky. Ve skutečnosti
existuje mnoho způsobů vložení
JavaScriptu.
Podívejme se na příklad vložení pomocí HTML atributu. Představte si fotogalerii, kde lze k obrázkům přidávat
popisky, které se vypíší v atributu alt
:
echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';
Útočníkovi stačí jako popisek vložit chytře sestavený řetězec " onload="alert('Hacked!')
. Pokud není
výstup ošetřen, výsledný kód bude vypadat takto:
<img src="photo0145.webp" alt="" onload="alert('Hacked!')">
Do stránky se tak dostává podvržený atribut onload
. Prohlížeč spustí kód v tomto atributu ihned po
načtení obrázku. A útočník opět získává kontrolu.
Jak se bránit XSS?
Pokusy detekovat útok pomocí blacklistu, například blokováním řetězce <script>
, jsou nedostatečné.
Základem účinné obrany je důsledná sanitizace všech dat vypisovaných na stránce.
Klíčovým prvkem je nahrazení všech znaků se speciálním významem za odpovídající sekvence. Tento proces se nazývá
escapování. Například v HTML textu má speciální význam znak <
, který musí být nahrazen HTML
entitou <
, pokud nemá být interpretován jako začátek tagu.
Zásadní je rozlišovat kontext, ve kterém data vypisujeme. V různých kontextech se totiž řetězce sanitizují odlišně, protože různé znaky mají speciální význam. Liší se například escapování v textu HTML, v atributech HTML, uvnitř některých speciálních elementů atd. Podrobněji se na to podíváme později.
Ošetření je nejlepší provádět přímo při vypsání řetězce na stránce. Tím zajistíme, že se skutečně provede, a to právě jednou. Ideální je, když ošetření provádí automaticky přímo šablonovací systém. Pokud ošetření není automatické, může na něj programátor snadno zapomenout. A jediné opomenutí stačí k tomu, aby byl web zranitelný.
XSS se netýká jen vypisování dat v šablonách, ale i dalších částí aplikace, které musí správně zacházet
s nedůvěryhodnými daty. Například je nutné, aby JavaScript ve vaší aplikaci nepoužíval ve spojení s nimi
innerHTML
, ale pouze innerText
nebo textContent
. Zvláštní pozornost je třeba věnovat
funkcím, které vyhodnocují řetězce jako JavaScript, což je eval()
, ale také setTimeout()
,
případně použití funkce setAttribute()
s eventovými atributy jako onload
. Toto však již
přesahuje oblast, kterou pokrývají šablony.
Ideální obrana ve třech bodech:
- rozpozná kontext, ve kterém se data vypisují
- sanitizuje data podle pravidel daného kontextu (tedy „kontextově sensitivně“)
- dělá to automaticky
Kontextově sensitivní escapování
Co přesně znamená slovo kontext? Jde o místo v dokumentu s vlastními pravidly pro ošetřování vypisovaných dat. Odvíjí se od typu dokumentu (HTML, XML, CSS, JavaScript, plain text, …) a může se lišit v jeho konkrétních částech.
V HTML dokumentu existuje celá řada míst (kontextů) s velmi odlišnými pravidly. Možná budete překvapeni, kolik jich je. Zde je první čtveřice:
<p>#text</p>
<img src="#atribut">
<textarea>#rawtext</textarea>
<!-- #komentář -->
Výchozím a základním kontextem HTML stránky je HTML text. Jaká zde platí pravidla? Speciální význam mají znaky
<
a &
, které představují začátek značky nebo entity. Musíme je escapovat nahrazením za
HTML entity (<
za <
, &
za &
).
Druhým nejběžnějším kontextem je hodnota HTML atributu. Od textu se liší tím, že speciální význam zde mají
uvozovky "
nebo '
, které atribut ohraničují. Ty je třeba zapsat jako entity, aby nebyly chápány
jako konec atributu. Naopak v atributu lze bezpečně používat znak <
, protože zde nemá žádný speciální
význam. Pozor však na to, že v HTML lze psát hodnoty atributů i bez uvozovek. V takovém případě má speciální
význam celá řada znaků, jde tedy o další samostatný kontext.
Možná vás překvapí, ale speciální pravidla platí i uvnitř elementů <textarea>
a
<title>
, kde se znak <
nemusí (ale může) escapovat, pokud za ním nenásleduje
/
. To je však spíše zajímavost.
Zajímavé je to uvnitř HTML komentářů. Tady se totiž k escapování nepoužívají HTML entity. Dokonce žádná specifikace neuvádí, jak by se mělo v komentářích escapovat. Jen je nutné dodržet poněkud kuriozní pravidla a vyhnout se v nich určitým kombinacím znaků.
Kontexty se mohou také vrstvit, k čemuž dochází, když vložíme JavaScript nebo CSS do HTML. To lze udělat dvěma odlišnými způsoby, elementem a atributem:
<script>#js-element</script>
<img onclick="#js-atribut">
<style>#css-element</style>
<p style="#css-atribut"></p>
Dvě cesty a dva různé způsoby escapování dat. Uvnitř elementů <script>
a <style>
se stejně jako v případě HTML komentářů escapování pomocí HTML entit neprovádí. Při vypisování dat uvnitř těchto
elementů je potřeba dodržet jediné pravidlo: text nesmí obsahovat sekvenci </script>
resp.
</style>
.
Naopak v atributech style
a on***
se pomocí HTML entit escapuje.
A samozřejmě uvnitř vnořeného JavaScriptu nebo CSS platí escapovací pravidla těchto jazyků. Řetězec v atributu
například onload
se nejprve escapuje podle pravidel JS a potom podle pravidel HTML atributu.
Jak vidíte, HTML je velmi komplexní dokument, kde se vrství kontexty. Bez uvědomění si, kde přesně data vypisuji (tj. v jakém kontextu), nelze říct, jak to správně udělat.
Chcete příklad?
Vezměme řetězec Rock'n'Roll
.
Pokud jej budete vypisovat v HTML textu, v tomto konkrétním případě není třeba provádět žádné záměny, protože řetězec neobsahuje žádný znak se speciálním významem. Jiná situace nastane, pokud jej vypíšete uvnitř HTML atributu ohraničeného jednoduchými uvozovkami. V takovém případě je potřeba escapovat uvozovky na HTML entity:
<div title='Rock'n'Roll'></div>
To bylo jednoduché. Mnohem zajímavější situace nastane při vrstvení kontextů, například pokud řetězec bude součástí JavaScriptu.
Nejprve jej vypíšeme do samotného JavaScriptu. Obalíme jej do uvozovek a zároveň escapujeme pomocí znaku \
uvozovky v něm obsažené:
'Rock\'n\'Roll'
Doplňme volání nějaké funkce, ať kód něco dělá:
alert('Rock\'n\'Roll');
Pokud tento kód vložíme do HTML dokumentu pomocí <script>
, není třeba nic dalšího upravovat,
protože se v něm nevyskytuje zakázaná sekvence </script>
:
<script> alert('Rock\'n\'Roll'); </script>
Pokud bychom jej však chtěli vložit do HTML atributu, musíme ještě escapovat uvozovky na HTML entity:
<div onclick='alert('Rock\'n\'Roll')'></div>
Vnořeným kontextem ale nemusí být jen JS nebo CSS. Běžně jím je také URL. Parametry v URL se escapují tak, že se
znaky se speciálním významem převádějí na sekvence začínající %
. Příklad:
https://example.org/?a=Jazz&b=Rock%27n%27Roll
A když tento řetězec vypíšeme v atributu, ještě aplikujeme escapování podle tohoto kontextu a nahradíme
&
za &
:
<a href="https://example.org/?a=Jazz&b=Rock%27n%27Roll">
Pokud jste dočetli až sem, gratulujeme, bylo to vyčerpávající. Teď už máte dobrou představu o tom, co jsou to kontexty a escapování. A nemusíte mít obavy, že je to složité. Latte tohle totiž dělá za vás automaticky.
Latte vs naivní systémy
Ukázali jsme si, jak se správně escapuje v HTML dokumentu a jak zásadní je znalost kontextu, tedy místa, kde data vypisujeme. Jinými slovy, jak funguje kontextově sensitivní escapování. Ačkoliv jde o nezbytný předpoklad funkční obrany před XSS, Latte je jediný šablonovací systém pro PHP, který tohle umí.
Jak je to možné, když všechny systémy dnes tvrdí, že mají automatické escapování? Automatické escapování bez znalosti kontextu je poněkud zavádějící a vytváří falešný dojem bezpečí.
Šablonovací systémy jako Twig, Laravel Blade a další nevidí v šabloně žádnou HTMLstrukturu. Nevidí tudíž ani kontexty. Oproti Latte jsou slepé a naivní. Zpracovávají jen vlastní značky, vše ostatní je pro ně nepodstatný tok znaků:
░░░░░░░░░░░░░░░░░{{ 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 }} -->
Naivní systémy pouze mechanicky převádějí znaky < > & ' "
na HTML entity, což je sice ve
většině případů platný způsob escapování, ale zdaleka ne vždy. Nemohou tak odhalit ani předejít vzniku různých
bezpečnostních děr, jak si ukážeme dále.
Latte šablonu vidí stejně jako vy. Chápe HTML, XML, rozeznává značky, atributy atd. A díky tomu rozlišuje jednotlivé kontexty a podle nich ošetřuje data. Nabízí tak opravdu efektivní ochranu proti kritické zranitelnosti Cross-site Scripting.
Živá ukázka
Vlevo vidíte šablonu v Latte, vpravo je vygenerovaný HTML kód. Několikrát se tu vypisuje proměnná $text
a
pokaždé v trošku jiném kontextu. A tedy i trošku jinak escapovaná. Kód šablony můžete sami editovat, například
změnit obsah proměnné atd. Zkuste si to:
Není to skvělé! Latte provádí kontextově sensitivní escapování automaticky, takže programátor:
- nemusí přemýšlet ani vědět, jak se kde escapuje
- nemůže se splést
- nemůže na escapování zapomenout
Tohle dokonce nejsou všechny kontexty, které Latte při vypisování rozlišuje a pro které přizpůsobuje ošetření dat. Další zajímavé případy si projdeme nyní.
Jak hacknout naivní systémy
Nyní si na praktických příkladech ukážeme, proč je rozlišování kontextů tak důležité a proč naivní šablonovací systémy neposkytují dostatečnou ochranu před XSS, na rozdíl od Latte. V ukázkách použijeme Twig jako zástupce naivních systémů, ale podobné principy platí i pro ostatní systémy.
Zranitelnost pomocí atributu
Pokusme se injektovat škodlivý kód do stránky pomocí HTML atributu. Představme si šablonu v Twigu pro vykreslení obrázku:
<img src={{ imageFile }} alt={{ imageAlt }}>
Všimněte si chybějících uvozovek kolem hodnot atributů. Takové opomenutí se může snadno stát, zejména když vývojář střídá různé technologie (např. v Reactu se atributy píší bez uvozovek).
Útočník by mohl jako popisek obrázku vložit řetězec foo onload=alert('Hacked!')
. Twig nedokáže rozpoznat
kontext, ve kterém se proměnná vypisuje, a pouze mechanicky převede znaky < > & ' "
na HTML entity.
Výsledný kód bude vypadat takto:
<img src=photo0145.webp alt=foo onload=alert('Hacked!')>
Vznikla bezpečnostní díra! Do stránky se dostal podvržený atribut onload
, který prohlížeč spustí
ihned po načtení obrázku.
Srovnejme to s Latte:
<img src={$imageFile} alt={$imageAlt}>
Latte chápe strukturu HTML a rozpozná, že proměnná se vypisuje jako hodnota atributu bez uvozovek. Proto je automaticky doplní. I když útočník vloží stejný škodlivý řetězec, výsledek bude bezpečný:
<img src="photo0145.webp" alt="foo onload=alert('Hacked!')">
Latte úspěšně odvrátilo XSS útok.
Bezpečné použití proměnných v JavaScriptu
Díky kontextově citlivému escapování můžete v Latte bezpečně používat PHP proměnné přímo v JavaScriptu:
<p onclick="alert({$movie})">{$movie}</p>
<script>var movie = {$movie};</script>
Pokud $movie
obsahuje řetězec 'Amarcord & 8 1/2'
, Latte vygeneruje následující kód:
<p onclick="alert("Amarcord & 8 1\/2")">Amarcord & 8 1/2</p>
<script>var movie = "Amarcord & 8 1\/2";</script>
Všimněte si, jak Latte použilo různé metody escapování pro HTML text, JavaScript v atributu onclick
a
JavaScript uvnitř tagu <script>
.
Kontrola odkazů
Latte jde v ochraně ještě dál. Automaticky kontroluje obsah atributů src
a href
, aby zabránilo
potenciálně nebezpečným odkazům:
{var $link = 'javascript:attack()'}
<a href={$link}>klikni</a>
Latte rozpozná nebezpečný odkaz a vypíše:
<a href="">klikni</a>
Tuto kontrolu lze v případě potřeby vypnout pomocí filtru nocheck.
Limity Latte
I když Latte poskytuje vynikající ochranu proti XSS, není to všelék na všechny bezpečnostní problémy webových aplikací. Bylo by nebezpečné, kdyby vývojáři při použití Latte přestali o bezpečnosti přemýšlet.
Latte se zaměřuje na to, aby útočník nemohl:
- Změnit strukturu HTML stránky
- Vložit nežádoucí HTML elementy nebo atributy
- Spustit JavaScript v kontextu, kde by neměl
Co Latte nekontroluje:
- Obsahovou správnost vypisovaných dat
- Logiku a správnost chování JavaScriptu
- Bezpečnost na úrovni aplikační logiky
Tyto aspekty zůstávají v kompetenci vývojáře. Je crucial věnovat pozornost validaci a sanitizaci vstupních dat, zejména těch pocházejících od uživatelů. Také je důležité pravidelně aktualizovat všechny komponenty aplikace a sledovat nové bezpečnostní hrozby.
Latte poskytuje silný první obranný val proti XSS útokům, ale komplexní bezpečnost vyžaduje holistický přístup zahrnující správné praktiky v celém vývojovém procesu.