Kvíz: meg tudja védeni magát az XSS sebezhetőségtől?

2 éve írta David Grudl  

Tesztelje tudását a biztonsági kvízben! Meg tudja akadályozni, hogy egy támadó átvegye az irányítást egy HTML oldal felett?

Minden feladatban ugyanazt a kérdést fogja megoldani: hogyan kell helyesen kiírni a $str változót egy HTML oldalon, hogy ne keletkezzen XSS sebezhetőség? A védekezés alapja az escapelés, ami azt jelenti, hogy a speciális jelentéssel bíró karaktereket a megfelelő szekvenciákkal helyettesítjük. Például, amikor egy stringet írunk ki HTML szövegbe, amelyben a < karakternek speciális jelentése van (egy tag kezdetét jelzi), helyettesítjük a &lt; HTML entitással, és a böngésző helyesen jeleníti meg a < szimbólumot.

Legyen óvatos, mert az XSS sebezhetőség nagyon súlyos. Okozhatja, hogy egy támadó átveszi az irányítást az oldal vagy akár a felhasználói fiók felett. Sok sikert, és sikerüljön biztonságban tartani a HTML oldalt!

Az első három kérdés

Adja meg, milyen karaktereket és milyen módon kell kezelni az első, második és harmadik példában:

1) <p><?= $str ?></p>
2) <input value="<?= $str ?>">
3) <input value='<?= $str ?>'>

Ha a kimenet semmilyen módon nem lenne kezelve, a megjelenített oldal részévé válna. Ha egy támadó a 'foo" onclick="evilCode()' stringet juttatná a változóba, és a kimenet nem lenne kezelve, azt okozná, hogy az elemre kattintva lefut a kódja:

$str = 'foo" onclick="evilCode()'
❌ kezelés nélkül: <input value="foo" onclick="evilCode()">
✅ kezeléssel:  <input value="foo&quot; onclick=&quot;evilCode()">

Az egyes példák megoldása:

  1. a < és & karakterek a HTML tag és entitás kezdetét jelentik, helyettesítjük őket &lt;-vel és &amp;-mal
  2. a " és & karakterek az attribútum értékének végét és a HTML entitás kezdetét jelentik, helyettesítjük őket &quot;-tal és &amp;-mal
  3. az ' és & karakterek az attribútum értékének végét és a HTML entitás kezdetét jelentik, helyettesítjük őket &apos;-sal és &amp;-mal

Minden helyes válaszért egy pontot kap. Természetesen mindhárom esetben lehet más karaktereket is entitásokkal helyettesíteni, ez nem árt, de nem szükséges.

4. kérdés

Folytatjuk tovább. Milyen karaktereket kell helyettesíteni a változó kiírásakor ebben a kontextusban?

<input value=<?= $str ?>>

Megoldás: Ahogy látja, itt hiányoznak az idézőjelek. A legegyszerűbb egyszerűen kiegészíteni az idézőjeleket, majd ugyanúgy escapelni, mint az előző kérdésben. Létezik egy második megoldás is, mégpedig a stringben a szóközt és minden olyan karaktert, amelynek speciális jelentése van a tagen belül, azaz >, /, = és néhány másikat, HTML entitásokkal helyettesíteni.

5. kérdés

Most már kezd érdekesebbé válni. Milyen karaktereket kell kezelni ebben a kontextusban:

<script>
	let foo = '<?= $str ?>';
</script>

Megoldás: A <script> tagen belül az escapelési szabályokat a JavaScript határozza meg. HTML entitások itt nem használatosak, azonban érvényes egy speciális szabály. Tehát milyen karaktereket escapelünk? Egy JavaScript stringen belül természetesen escapeljük az ' karaktert, amely azt határolja, mégpedig perjel segítségével, tehát \'-re cseréljük. Mivel a JavaScript nem támogatja a többsoros stringeket (csak template literal), escapelnünk kell a sorvégi karaktereket is. Azonban vigyázat, a szokásos \n és \r karaktereken kívül a JavaScript a \u2028 és \u2029 unicode karaktereket is sorvégnek tekinti, amelyeket szintén escapelnünk kell. És végül az említett speciális szabály: a stringben nem fordulhat elő </script. Ezt meg lehet akadályozni pl. <\/script-re való cserével.

Ha ezt tudta, gratulálunk.

6. kérdés

A következő kontextus csak az előző variációjának tűnik. Mit gondol, eltérő lesz a kezelés?

<p onclick="foo('<?= $str ?>')"></p>

Megoldás: Ismét a JavaScript stringekben való escapelés szabályai érvényesek itt, de ellentétben az előző kontextussal, ahol HTML entitásokkal nem escapeltünk, itt éppen ellenkezőleg, escapelünk. Tehát először escapeljük a JavaScript stringet perjelekkel, majd a speciális jelentéssel bíró karaktereket (" és &) HTML entitásokkal helyettesítjük. Vigyázat, a helyes sorrend fontos.

Ahogy látja, ugyanaz a JavaScript literál másképp lehet kódolva a <script> elemben és másképp egy attribútumban!

7. kérdés

Visszatérünk a JavaScriptből a HTML-be. Milyen karaktereket kell helyettesítenünk a kommenten belül és milyen módon?

<!-- <?= $str ?> -->

Megoldás: a HTML (és XML) kommenten belül az összes hagyományos speciális karakter, mint a <, &, " és ', előfordulhat. Tiltott, és ez valószínűleg meglepi, a -- karakterpár. Ennek a szekvenciának az escapelése nincs specifikálva, így önön múlik, milyen módon helyettesíti. Közéjük tehet szóközöket. Vagy például helyettesítheti ==-vel.

8. kérdés

Már közeledünk a végéhez, próbáljuk megváltoztatni a kérdést. Próbáljon meg elgondolkodni azon, mire kell figyelni a változó kiírásakor ebben a kontextusban:

<a href="<?= $str ?>">...</a>

Megoldás: az escapelésen kívül fontos még ellenőrizni, hogy az URL nem tartalmaz-e veszélyes sémát, mint a javascript:, mert egy így összeállított URL kattintás után a támadó kódját hívná meg.

9. kérdés

Végül egy csemege az igazi ínyenceknek. Egy modern JavaScript keretrendszert, konkrétan a Vue-t használó alkalmazás példája. Kíváncsi vagyok, eszébe jut-e, mire kell figyelni a változó kiírásakor az #app elemen belül:

<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>

Ez a kód egy Vue alkalmazást hoz létre, amely az #app elembe fog renderelődni. A Vue ennek az elemnek a tartalmát a saját sablonjaként értelmezi. És a sablonon belül értelmezi a dupla kapcsos zárójeleket, amelyek egy változó kiírását vagy egy JavaScript kód meghívását jelentik (pl. {{ foo }}).

Tehát az #app elemen belül a < és & karaktereken kívül még speciális jelentése van a {{ párnak, amelyet egy másik megfelelő szekvenciával kell helyettesítenünk, hogy a Vue ne értelmezze a saját tagjeként. A HTML entitásokkal való helyettesítés azonban ebben az esetben nem segít. Hogyan birkózzunk meg vele? Működik egy trükk: a zárójelek közé egy üres HTML kommentet illesztünk {<!-- -->{, és a Vue egy ilyen szekvenciát figyelmen kívül hagy.

Kvíz eredményei

Hogy teljesített a kvízben? Hány helyes válasza van? Ha legalább 4 kérdésre helyesen válaszolt, a legjobb 8% közé tartozik – gratulálunk!

Azonban webhelye biztonságának biztosításához elengedhetetlen a kimenet helyes kezelése minden helyzetben.

Ha meglepte, mennyi különböző kontextus fordulhat elő egy átlagos HTML oldalon, akkor tudja, hogy messze nem említettük mindet. Az egy sokkal hosszabb kvíz lenne. Ennek ellenére nem kell szakértőnek lennie az escapelésben minden kontextusban, ha a sablonrendszere képes rá.

Tehát próbáljuk ki őket.

Hogyan teljesítenek a sablonrendszerek?

Minden modern sablonrendszer büszkélkedik az automatikus escapelés funkcióval, amely automatikusan escapeli az összes kiírt változót. Ha ezt helyesen teszik, a webhelye biztonságban van. Ha rosszul teszik, a webhely ki van téve az XSS sebezhetőség kockázatának, annak minden súlyos következményével.

Teszteljük a népszerű sablonrendszereket ennek a kvíznek a kérdéseivel, hogy megtudjuk, mennyire hatékony az automatikus escapelésük. Kezdődik a PHP sablonrendszerek dTestje.

Twig ❌

Az első a sorban a Twig (3.5-ös verzió) sablonrendszer, amelyet leggyakrabban a Symfony keretrendszerrel együtt használnak. Feladjuk neki a feladatot, hogy válaszoljon az összes kvízkérdésre. A $str változó mindig egy trükkös stringgel lesz feltöltve, és megnézzük, hogyan birkózik meg a kiírásával. Az eredményeket jobbra látja. Válaszait és viselkedését a játszótéren is megvizsgálhatja.

   {% 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>

A Twig kilenc tesztből hatban megbukott!

Sajnos a Twig automatikus escapelése csak HTML szövegben és attribútumokban működik, és ráadásul csak akkor, ha idézőjelek közé vannak zárva. Amint hiányoznak az idézőjelek, a Twig nem jelez hibát, és létrehoz egy XSS biztonsági rést.

Ez különösen kellemetlen, mert így írják az attribútumértékeket olyan népszerű könyvtárakban, mint a React vagy a Svelte. Egy programozó, aki egyszerre használja a Twiget és a Reactot, így teljesen természetesen elfelejtheti az idézőjeleket.

A Twig automatikus escapelése minden más példában is megbukik. Az (5) és (6) kontextusokban manuálisan kell escapelni a {{ str|escape('js') }} segítségével, további kontextusokhoz a Twig escapelési funkciót sem kínál. Nem rendelkezik védelemmel a hibás link kiírása ellen (8), sem támogatással a Vue sablonokhoz (9).

Blade ❌❌

A második résztvevő a Blade (10.9-es verzió) sablonrendszer, amely szorosan integrálva van a Laravellel és annak ökoszisztémájával. Ismét ellenőrizzük képességeit a kvízkérdéseinken. Válaszait a játszótéren is megvizsgálhatja.

   @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>

A Blade kilenc tesztből hatban megbukott!

Az eredmény hasonló a Twigéhez. Ismét érvényes, hogy az automatikus escapelés csak HTML szövegben és attribútumokban működik, és csak akkor, ha idézőjelek közé vannak zárva. A Blade automatikus escapelése minden más példában is megbukik. Az (5) és (6) kontextusokban manuálisan kell escapelni a {{ Js::from($str) }} segítségével. További kontextusokhoz a Blade escapelési funkciót sem kínál. Nem rendelkezik védelemmel a hibás link kiírása ellen (8), sem támogatással a Vue sablonokhoz (9).

Ami azonban meglepő, az a @php direktíva hibája a Blade-ben, ami azt okozza, hogy a saját PHP kódja közvetlenül a kimenetre kerül, amit az utolsó sorban láthat.

Smarty ❌❌❌

Most a legrégebbi PHP sablonrendszert teszteljük, ami a Smarty (4.3-as verzió). Nagy meglepetésre ennek a rendszernek nincs aktív automatikus escapelése. Így a változók kiírásakor vagy minden alkalommal meg kell adni a {$var|escape} szűrőt, vagy aktiválni kell az automatikus HTML escapelést. Erről az információ a dokumentációban elég eldugott helyen van.

   {$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>

A Smarty kilenc tesztből hatban megbukott!

Az eredmény első pillantásra hasonló az előző könyvtárakéhoz. A Smarty csak HTML szövegben és attribútumokban tud automatikusan escapelni, és csak akkor, ha az értékek idézőjelek közé vannak zárva. Mindenhol máshol megbukik. Az (5) és (6) kontextusokban manuálisan kell escapelni a {$str|escape:javascript} segítségével. De ez csak akkor lehetséges, ha nincs aktiválva az automatikus HTML escapelés, különben ezek az escapelések ütköznek egymással. A Smarty így biztonsági szempontból ennek a tesztnek a teljes bukása.

Latte ✅

A hármast a Latte (3.0-s verzió) sablonrendszer zárja. Kipróbáljuk az autoescapelését. Válaszait és viselkedését a játszótéren is megvizsgálhatja.

   {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>

A Latte mind a kilenc feladatban kiválóan teljesített!

Megbirkózott a hiányzó idézőjelekkel a HTML attribútumoknál, kezelte a JavaScriptet mind a <script> elemben, mind az attribútumokban, és megbirkózott a tiltott szekvenciával a HTML kommentekben is.

Sőt, megelőzte azt a helyzetet, hogy egy támadó által becsempészett linkre kattintva lefusson a kódja. És megbirkózott a Vue tagjeinek escapelésével is.

Bónusz teszt

Minden sablonrendszer egyik lényeges képessége a blokkokkal való munka és az ehhez kapcsolódó sablonöröklődés. Ezért megpróbálunk még egy feladatot adni az összes tesztelt sablonrendszernek. Létrehozunk egy description blokkot, amelyet egy HTML attribútumban írunk ki. Valós világban természetesen a blokk definíciója a gyermek-sablonban, kiírása pedig a szülő-sablonban, tehát például a layoutban található lenne. Ez csak egy egyszerűsített forma, de elegendő ahhoz, hogy teszteljük az automatikus escapelést a blokkok kiírásakor. Hogyan teljesítettek?

Twig: megbukott ❌ a blokkok kiírásakor nem kezeli a karaktereket

{% block description %}
	rock n' roll
{% endblock %}

<meta name='description'
	content='{{ block('description') }}'>




<meta name='description'
	content=' rock n' roll '> ❌

Blade: megbukott ❌ a blokkok kiírásakor nem kezeli a karaktereket

@section('description')
	rock n' roll
@endsection

<meta name='description'
	content='@yield('description')'>




<meta name='description'
	content=' rock n' roll '> ❌

Latte: megfelelt ✅ a blokkok kiírásakor helyesen kezelte a problémás karaktereket

{block description}
	rock n' roll
{/block}

<meta name='description'
	content='{include description}'>




<meta name='description'
	content=' rock n&apos; roll '> ✅

Miért sebezhető ennyi webhely?

Az automatikus escapelés olyan rendszerekben, mint a Twig, Blade vagy Smarty, úgy működik, hogy egyszerűen helyettesíti az öt <>"'& karaktert HTML entitásokkal, és nem tesz különbséget a kontextusok között. Ezért csak bizonyos helyzetekben működik, és minden másban megbukik. A naiv automatikus escapelés veszélyes funkció, mert hamis biztonságérzetet kelt.

Nem meglepő tehát, hogy jelenleg a webhelyek több mint 27%-a rendelkezik kritikus sebezhetőségekkel, elsősorban XSS-sel (forrás: Acunetix Web Vulnerability Report). Hogyan lehet ebből kilábalni? Használjon olyan sablonrendszert, amely megkülönbözteti a kontextusokat.

A Latte az egyetlen PHP sablonrendszer, amely nem csak karakterláncként tekint a sablonra, hanem érti a HTML-t. Érti, mik a tagek, attribútumok stb. Megkülönbözteti a kontextusokat. És ezért helyesen escapel HTML szövegben, másképp egy HTML tagen belül, másképp JavaScripten belül stb.

A Latte így az egyetlen biztonságos sablonrendszer.


Sőt, a HTML megértésének köszönhetően a csodálatos n:attribútumokat is kínálja, amit a felhasználók imádnak:

<ul n:if="$menu">
	<li n:foreach="$menu->getItems() as $item">{$item->title}</li>
</ul>