Kvíz: ubráníte se před zranitelností XSS?
Otestujte své znalosti v bezpečnostním kvízu! Dokážete zabránit útočníkovi v převzetí kontroly nad HTML stránkou?
Ve všech úkolech budete řešit stejnou otázku: jak správně vypsat
proměnnou $str
v HTML stránce, aby nevznikla zranitelnost XSS. Základem
obrany je escapování, což znamená nahrazování znaků se
speciálním významem za odpovídající sekvence. Například při výpisu
řetězce do HTML textu, ve kterém má znak <
speciální
význam (signalizuje začátek značky), jej nahradíme HTML entitou
<
a prohlížeč správně zobrazí symbol
<
.
Buďte na pozoru, protože zranitelnost XSS je velmi vážná. Může způsobit, že útočník převezme kontrolu nad stránkou nebo dokonce uživatelským účtem. Hodně úspěchů a ať se vám daří udržet HTML stránku v bezpečí!
První trojice otázek
Uveďte, jaké znaky a jakým způsobem je třeba ošetřit v prvním, druhém a třetím příkladu:
1) <p><?= $str ?></p>
2) <input value="<?= $str ?>">
3) <input value='<?= $str ?>'>
Pokud by výstup nebyl nijak ošetřen, stal by se součástí zobrazené
stránky. Když by útočník dostal do proměnné řetězec
'foo" onclick="evilCode()'
a výstup by nebyl ošetřen, způsobil
by, že při kliknutí na element se spustí jeho kód:
$str = 'foo" onclick="evilCode()'
❌ bez ošetření: <input value="foo" onclick="evilCode()">
✅ s ošetřením: <input value="foo" onclick="evilCode()">
Řešení jednotlivých příkladů:
- znaky
<
a&
představují začátek HTML tagu a entity, nahradíme je za<
a&
- znaky
"
a&
představují konec hodnoty atributu a začátek HTML entity, nahradíme je za"
a&
- znaky
'
a&
představují konec hodnoty atributu a začátek HTML entity, nahradíme je za'
a&
Za každou správnou odpověď získáváte bod. Samozřejmě ve všech třech případech lze nahrazovat za entity i další znaky, ničemu to nevadí, ale není to nutné.
Otázka č. 4
Pokračujeme dál. Jaké znaky je potřeba nahradit při výpisu proměnné v tomto kontextu?
<input value=<?= $str ?>>
Řešení: Jak vidíte, tady chybí uvozovky. Nejsnazší je prostě uvozovky
doplnit a pak escapovat stejně jako v přechozí otázce. Existuje i druhé
řešení, a to nahradit v řetězci za HTML entity mezeru a všechny znaky,
které mají speciální význam uvnitř tagu, tj. >
,
/
, =
a některé
další.
Otázka č. 5
Teď už to začíná být zajímavější. Jaké znaky je potřeba ošetřit v tomto kontextu:
<script>
let foo = '<?= $str ?>';
</script>
Řešení: Uvnitř značky <script>
pravidla pro
escapování určuje JavaScript. HTML entity se tu nepoužívají,
nicméně platí jedno speciální pravidlo. Takže jaké znaky escapujeme?
Uvnitř JavaScriptového řetězce escapujeme samozřejmě znak '
,
který jej ohraničuje, a to pomocí lomítka, tedy nahradíme jej za
\'
. Jelikož JavaScript nepodporuje víceřádkové řetězce (jen
jako template
literal), musíme escapovat i znaky konce řádků. Ovšem pozor, krom
obvyklých znaků \n
a \r
považuje JavaScript za
konce řádků i unicode znaky \u2028
a \u2029
,
které musíme escapovat taktéž. A nakonec zmíněné speciální pravidlo:
v řetězci se nesmí vyskytnout </script
. Tomu se dá zamezit
např. nahrazením za <\/script
.
Pokud jste tohle věděli, gratulujeme.
Otázka č. 6
Následující kontext vypadá jen jako variace přechozího. Co myslíte, bude se ošetřování lišit?
<p onclick="foo('<?= $str ?>')"></p>
Řešení: Opět tu platí pravidla pro escapování v JavaScriptových
řetězcích, ale na rozdíl od předchozího kontextu, kde se pomocí HTML
entit neescapovalo, tady se naopak escapuje. Tedy nejprve escapujeme
JavaScriptový řetězec pomocí lomítek a poté nahradíme znaky se
speciálním významem ("
a &
) za HTML entity.
Pozor, správné pořadí je důležité.
Jak vidíte, stejný JavaScriptový literal se může být zakódovaný jinak
v elementu <script>
a jinak v atributu!
Otázka č. 7
Vrátíme se z JavaScriptu zpět do HTML. Jaké znaky musíme nahradit uvnitř komentáře a jakým způsobem?
<!-- <?= $str ?> -->
Řešení: uvnitř HTML (a XML) komentáře se všechny tradiční
speciální znaky, jako je <
, &
, "
a '
, objevovat mohou. Zakázaná je, a to vás asi překvapí,
dvojice znaků --
. Escapování této sekvence není
specifikováno, a tak je na vás, jakým způsobem ji nahradíte. Můžete je
proložit mezerami. Nebo třeba nahradit za ==
.
Otázka č. 8
Už se blížíme ke konci, tak zkusíme obměnit otázku. Zkuste se zamyslet, na co je nutné si dát pozor při vypsání proměnné v tomto kontextu:
<a href="<?= $str ?>">...</a>
Řešení: kromě escapování je důležité ještě ověřit, že URL
neobsahuje nebezpečné schéma jako javascript:
, protože takto
sestavené URL by po kliknutí zavolalo útočníkův kód.
Otázka č. 9
Na závěr perlička pro skutečné fajnšmekry. Jde o ukázku aplikace
využívající moderní JavaScriptový framework, konkrétně Vue. Schválně
jestli vás napadne, na co si dát pozor při výpisu proměnné uvnitř
elementu #app
:
<div id="app">
<?= $str ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
...
})
</script>
Tento kód vytváří Vue aplikaci, která se bude vykreslovat do elementu
#app
. Vue obsah tohoto elementu chápe jako svou šablonu.
A uvnitř šablony interpretuje
dvojité složené závorky, které znamenají výpis proměnné nebo zavolání
javascriptového kódu (např. {{ foo }}
).
Tedy uvnitř elementu #app
má kromě znaků <
a
&
ještě speciální význam dvojice {{
, kterou
musíme nahradit za jinou odpovídající sekvenci, aby ji Vue neinterpretovalo
jako svoji značku. Nahrazení za HTML entity ovšem v tomto případě
nepomůže. Jak si poradit? Funguje trik: mezi závorky vložíme prázdný HTML
komentář {<!-- -->{
a Vue takovou sekvenci ignoruje.
Výsledky kvízu
Jak jste si vedli v kvízu? Kolik správných odpovědí máte? Pokud jste na alespoň 4 otázky odpověděli správně, patříte mezi 8 % nejlepších řešitelů – gratulujeme!
Avšak pro zajištění bezpečnosti vašeho webu je nezbytné správně ošetřit výstup ve všech situacích.
Jestli vás překvapilo, kolik různých kontextů se může vyskytnout na běžné HTML stránce, tak vězte, že jsme nezmínili zdaleka všechny. To by byl kvíz mnohem delší. Přesto nemusíte být expertem na escapování v každém kontextu, pokud to zvládne váš šablonovací systém.
Pojďme je tedy vyzkoušet.
Jak si vedou šablonovací systémy?
Všechny moderní šablonovací systémy se pyšní funkcí autoescapování, která automaticky escapuje všechny vypisované proměnné. Pokud to dělají správně, váš web je v bezpečí. Pokud to dělají špatně, web je vystaven riziku zranitelnosti XSS se všemi vážnými důsledky.
Otestujeme oblíbené šablonovací systémy z otázek tohoto kvízu, abychom zjistili, jak efektivní je jejich autoescapování. Začíná dTest šablonovacích systémů pro PHP.
Twig ❌
První na řadě je šablonovací systém Twig (verze 3.5), který se nejčastěji
používá ve spojení s frameworkem Symfony. Dáme mu úkol zodpovědět
všechny kvízové otázky. Proměnná $str
bude vždy naplněna
záludným řetězcem a podíváme se, jak si poradí s jeho výpisem.
Výsledky vidíte napravo. Můžete jeho odpovědi a chování také prozkoumat na hřišti.
{% set str = "<'\"&" %}
1) <p>{{ str }}</p>
2) <input value="{{ str }}">
3) <input value='{{ str }}'>
{% set str = "foo onclick=evilCode()" %}
4) <input value={{ str }}>
{% set str = "'\"\n\u{2028}" %}
5) <script> let foo = '{{ str }}'; </script>
6) <p onclick="foo('{{ str }}')"></p>
{% set str = "-- ---" %}
7) <!-- {{ str }} -->
{% set str = "javascript:evilCode()" %}
8) <a href="{{ str }}">...</a>
{% set str = "{{ foo }}" %}
9) <div id="app"> {{ str }} </div>
✅ <p><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '"u{2028}; </script>
❌ <p onclick="foo('"u{2028})"></p>
❌ <!-- -- --- -->
❌ <a href="javascript:evilCode()">...</a>
❌ <div id="app"> {{ foo }} </div>
Twig selhal v šesti z devíti testů!
Bohužel, automatické escapování Twigu funguje pouze v HTML textu a atributech, a to navíc jen tehdy, pokud jsou uzavřené do uvozovek. Jakmile uvozovky chybí, Twig neohlásí žádnou chybu a vytvoří bezpečnostní díru XSS.
Toto je obzvláště nepříjemné, protože takto se hodnoty atributů zapisují v populárních knihovnách, jako jsou React nebo Svelte. Programátor, který zároveň používá Twig i React, tak zcela přirozeně může na uvozovky zapomenout.
Autoescapování Twigu selhává i ve všech ostatních příkladech.
V kontextech (5) a (6) je potřeba escapovat manuálně pomocí
{{ str|escape('js') }}
, pro další kontexty Twig escapovací
funkci ani nenabízí. Nedisponuje ani ochranou před vypsáním závadného
odkazu (8) nebo podporou šablon pro Vue (9).
Blade ❌❌
Druhým účastníkem je šablonovací systém Blade (verze 10.9), který je těsně integrován s Laravelem a jeho ekosystémem. Opět prověříme jeho schopnosti na našich kvízových otázkách. Jeho odpovědi si můžete také prozkoumat na hřišti.
@php($str = "<'\"&")
1) <p>{{ $str }}</p>
2) <input value="{{ $str }}">
3) <input value='{{ $str }}'>
@php($str = "foo onclick=evilCode()")
4) <input value={{ $str }}>
@php($str = "'\"\n\u{2028}")
5) <script> let foo = {{ $str }}; </script>
6) <p onclick="foo({{ $str }})"></p>
@php($str = "-- ---")
7) <!-- {{ $str }} -->
@php($str = "javascript:evilCode()")
8) <a href="{{ $str }}">...</a>
@php($str = "{{ foo }}")
9) <div id="app"> {{ $str }} </div>
✅ <p><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '" ; </script>
❌ <p onclick="foo('" )"></p>
❌ <!-- -- --- -->
❌ <a href="javascript:evilCode()">...</a>
❌❌ <div id="app"> <?php echo e(foo); ?> </div>
Blade selhal v šesti z devíti testů!
Výsledek je podobný jako u Twigu. Opět platí, že automatické
escapování funguje pouze v HTML textu a atributech a pouze pokud jsou
uzavřené do uvozovek. Autoescapování Blade selhává i ve všech ostatních
příkladech. V kontextech (5) a (6) je nutné escapovat manuálně pomocí
{{ Js::from($str) }}
. Pro další kontexty Blade escapovací funkci
ani nenabízí. Nedisponuje ochranou před vypsáním závadného odkazu (8) ani
podporou šablon pro Vue (9).
Co je však překvapující, je selhání direktivy @php
v Blade, což způsobuje vypsání vlastního PHP kódu přímo na výstup,
což vidíte v posledním řádku.
Smarty ❌❌❌
Nyní otestujeme nejstarší šablonovací systém pro PHP, kterým je Smarty (verze 4.3). K velkému překvapení
tento systém nemá aktivní automatické escapování. Musíte tak při
vypisování proměnných buď pokaždé uvést filtr
{$var|escape}
, nebo aktivovat automatické escapování HTML.
Informace o tom je v dokumentaci dosti zapadlá.
{$str = "<'\"&"}
1) <p>{$str}</p>
2) <input value="{$str}">
3) <input value='{$str}'>
{$str = "foo onclick=evilCode()"}
4) <input value={$str}>
{$str = "'\"\n\u{2028}"}
5) <script> let foo = {$str}; </script>
6) <p onclick="foo({$str})"></p>
{$str = "-- ---"}
7) <!-- {$str} -->
{$str = "javascript:evilCode()"}
8) <a href="{$str}">...</a>
{$str = "{{ foo }}"}
9) <div id="app"> {$str} </div>
✅ <p><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '"\u2028; </script>
❌ <p onclick="foo('"\u2028)"></p>
❌ <!-- -- --- -->
❌ <a href="javascript:evilCode()">...</a>
❌ <div id="app"> {{ foo }} </div>
Smarty selhaly v šesti z devíti testů!
Výsledek je na první pohled podobný jako u předchozích knihoven. Smarty
dokáží automaticky escapovat pouze v HTML textu a atributech, a to jen když
jsou hodnoty uzavřené do uvozovek. Všude jinde selhává. V kontextech (5) a
(6) je nutné escapovat manuálně pomocí
{$str|escape:javascript}
. Jenže to je možné pouze tehdy, když
není aktivní automatické escapování HTML, jinak se totiž tyto escapování
tlučou navzájem. Smarty jsou tak z pohledu bezpečnosti naprostý
propadák tohoto testu.
Latte ✅
Trojici uzavírá šablonovací systém Latte (verze 3.0). Vyzkoušíme jeho autoescapování. Jeho odpovědi a chování si můžete taktéž prozkoumat na hřišti.
{var $str = "<'\"&"}
1) <p>{$str}</p>
2) <input value="{$str}">
3) <input value='{$str}'>
{var $str = "foo onclick=evilCode()"}
4) <input value={$str}>
{var $str = "'\"\n\u{2028}"}
5) <script> let foo = {$str}; </script>
6) <p onclick="foo({$str})"></p>
{var $str = "-- ---"}
7) <!-- {$str} -->
{var $str = "javascript:evilCode()"}
8) <a href="{$str}">...</a>
{var $str = "{{ foo }}"}
9) <div id="app"> {$str} </div>
✅ <p><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
✅ <input value="foo onclick=evilCode()">
✅ <script> let foo = "'\"\n\u2028"; </script>
✅ <p onclick="foo("'\"\n\u2028")"></p>
✅ <!-- - - - - - -->
✅ <a href="">...</a>
✅ <div id="app"> {<!-- -->{ foo }} </div>
Latte ve všech devíti úkolech excelovalo!
Zvládlo se vyrovnat s chybějícími uvozovkami u HTML atributů, zvládlo
zpracovat JavaScript jak v elementu <script>
, tak
v atributech a dokázalo se vypořádat i se zakázanou sekvencí v HTML
komentářích.
Ba co víc, předešlo situaci, kdy by kliknutí na podvržený odkaz útočníkem mohlo spustit jeho kód. A zvládlo si poradit s escapováním značek pro Vue.
Bonusový test
Jedna z podstatných schopností všech šablonovacích systémů je práce
s bloky a s tím spojená dědičnost šablon. Zkusíme proto dát všem
testovaným šablonovacím systémům ještě jeden úkol. Vytvoříme blok
description
, který vypíšeme v HTML atributu. V reálném
světě by se samozřejmě definice bloku nacházela v child-šabloně a jeho
vypsání v parent-šabloně, tedy třeba layoutu. Toto je jen zjednodušená
podoba, ale stačí k tomu, abychom otestovali autoescapování při
vypisování bloků. Jak obstáli?
Twig: selhal ❌ při vypisování bloků znaky neošetřuje
{% block description %}
rock n' roll
{% endblock %}
<meta name='description'
content='{{ block('description') }}'>
<meta name='description'
content=' rock n' roll '> ❌
Blade: selhal ❌ při vypisování bloků znaky neošetřuje
@section('description')
rock n' roll
@endsection
<meta name='description'
content='@yield('description')'>
<meta name='description'
content=' rock n' roll '> ❌
Latte: obstál ✅ při vypisování bloků korektně ošetřil problematické znaky
{block description}
rock n' roll
{/block}
<meta name='description'
content='{include description}'>
<meta name='description'
content=' rock n' roll '> ✅
Proč je tolik webů zranitelných?
Autoescapování v systémech jako Twig, Blade nebo Smarty funguje tak, že
jednoduše nahrazuje pět znaků <>"'&
za HTML entity a
nijak nerozlišuje kontext. Proto funguje pouze v některých situacích a ve
všech ostatních selhává. Naivní autoescapování je nebezpečná funkce,
protože vytváří falešný pocit bezpečí.
Není proto překvapivé, že v současnosti má více než 27 % webů kritické zranitelnosti, především XSS (zdroj: Acunetix Web Vulnerability Report). Jak z toho ven? Použít šablonovací systém, který rozlišuje kontexty.
Latte je jediný šablonovací systém v PHP, který nevnímá šablonu jen jako řetězec znaků, ale rozumí HTML. Chápe, co jsou značky, atributy atd. Rozlišuje kontexty. A proto správně escapuje v HTML textu, jinak uvnitř HTML značky, jinak uvnitř JavaScriptu atd.
Latte tak představuje jediný bezpečný šablonovací systém.
Navíc díky tomu, že chápe HTML, nabízí ještě báječné n:atributy, které uživatelé milují:
<ul n:if="$menu">
<li n:foreach="$menu->getItems() as $item">{$item->title}</li>
</ul>
Komentáře
<3
♥
nice
Chcete-li odeslat komentář, přihlaste se