Kvíz: ubráníte se před zranitelností XSS?

před rokem od David Grudl  

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 &lt; 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&quot; onclick=&quot;evilCode()">

Řešení jednotlivých příkladů:

  1. znaky < a & představují začátek HTML tagu a entity, nahradíme je za &lt;&amp;
  2. znaky " a & představují konec hodnoty atributu a začátek HTML entity, nahradíme je za &quot;&amp;
  3. znaky ' a & představují konec hodnoty atributu a začátek HTML entity, nahradíme je za &apos;&amp;

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>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script> let foo = &#039;&quot;u{2028}; </script>
❌ <p onclick="foo(&#039;&quot;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>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script>	let foo = &#039;&quot; ; </script>
❌ <p onclick="foo(&#039;&quot; )"></p>


❌ <!-- -- --- -->


❌ <a href="javascript:evilCode()">...</a>


❌❌ <div id="app"> &lt;?php echo e(foo); ?&gt; </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>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script> let foo = &#039;&quot;\u2028; </script>
❌ <p onclick="foo(&#039;&quot;\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>&lt;'"&amp;</p>
✅ <input value="&lt;&apos;&quot;&amp;">
✅ <input value='&lt;&apos;&quot;&amp;'>


✅ <input value="foo onclick=evilCode()">


✅ <script> let foo = "'\"\n\u2028"; </script>
✅ <p onclick="foo(&quot;&apos;\&quot;\n\u2028&quot;)"></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&apos; 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 (RSS)

  1. <3

    před rokem
  2. před rokem
  3. nice

    před 12 měsíci

Chcete-li odeslat komentář, přihlaste se