A Latte a biztonság szinonimája

A Latte az egyetlen PHP sablonrendszer, amely hatékony védelmet nyújt a kritikus Cross-site Scripting (XSS) sebezhetőség ellen. Ez az úgynevezett kontextusérzékeny escapelésnek köszönhető. Elmondjuk,

  • mi az XSS sebezhetőség elve és miért olyan veszélyes
  • miért olyan hatékony a Latte az XSS elleni védekezésben
  • hogyan lehet könnyen biztonsági rést létrehozni a Twig, Blade és hasonló sablonokban

Cross-site Scripting (XSS)

A Cross-site Scripting (röviden XSS) az egyik leggyakoribb weboldal sebezhetőség, és egyben nagyon veszélyes is. Lehetővé teszi a támadó számára, hogy rosszindulatú szkriptet (ún. malware-t) illesszen be egy idegen oldalra, amely a gyanútlan felhasználó böngészőjében fut le.

Mit tehet egy ilyen szkript? Például elküldheti a támadónak a megtámadott oldal bármely tartalmát, beleértve a bejelentkezés után megjelenített érzékeny adatokat is. Módosíthatja az oldalt, vagy további kéréseket hajthat végre a felhasználó nevében. Ha például egy webmailről lenne szó, elolvashatja az érzékeny üzeneteket, módosíthatja a megjelenített tartalmat, vagy átállíthatja a konfigurációt, pl. bekapcsolhatja az összes üzenet másolatának továbbítását a támadó címére, hogy hozzáférjen a jövőbeli e-mailekhez is.

Ezért szerepel az XSS a legveszélyesebb sebezhetőségek rangsorának élén. Ha egy weboldalon sebezhetőség jelenik meg, azt a lehető leghamarabb el kell távolítani a visszaélések megelőzése érdekében.

Hogyan keletkezik a sebezhetőség?

A hiba ott keletkezik, ahol a weboldal generálódik és a változók kiíródnak. Képzelje el, hogy egy keresőoldalt hoz létre, és az elején egy bekezdés található a keresett kifejezéssel a következő formában:

echo '<p>Keresési eredmények erre: <em>' . $search . '</em></p>';

A támadó a keresőmezőbe és ezáltal a $search változóba bármilyen stringet beírhat, tehát HTML kódot is, mint <script>alert("Hacked!")</script>. Mivel a kimenet nincs semmilyen módon kezelve, a megjelenített oldal részévé válik:

<p>Keresési eredmények erre: <em><script>alert("Hacked!")</script></em></p>

A böngésző ahelyett, hogy kiírná a keresett stringet, futtatja a JavaScriptet. És ezzel a támadó átveszi az irányítást az oldal felett.

Ellenvethetné, hogy a kód beillesztése a változóba ugyan futtatja a JavaScriptet, de csak a támadó böngészőjében. Hogyan jut el az áldozathoz? Ebből a szempontból több XSS típust különböztetünk meg. A keresési példánkban reflected XSS-ről beszélünk. Itt még rá kell venni az áldozatot, hogy kattintson egy linkre, amely a rosszindulatú kódot tartalmazza a paraméterben:

https://example.com/?search=<script>alert("Hacked!")</script>

A felhasználó rávezetése a linkre ugyan igényel némi social engineeringet, de ez nem túl bonyolult. A felhasználók gondolkodás nélkül kattintanak a linkekre, legyen az e-mailben vagy közösségi oldalakon. És hogy a címben valami gyanús van, azt el lehet rejteni egy URL rövidítővel, a felhasználó csak annyit lát: bit.ly/xxx.

Azonban létezik egy második, sokkal veszélyesebb támadási forma is, az úgynevezett stored XSS vagy persistent XSS, amikor a támadónak sikerül a rosszindulatú kódot a szerveren tárolni úgy, hogy az automatikusan beillesztődjön néhány oldalba.

Például olyan oldalak, ahová a felhasználók kommenteket írnak. A támadó elküld egy kódot tartalmazó hozzászólást, és az elmentődik a szerverre. Ha az oldalak nincsenek megfelelően védve, akkor minden látogató böngészőjében lefut.

Úgy tűnhet, hogy a támadás lényege az, hogy a <script> stringet bejuttassuk az oldalra. Valójában a JavaScript beillesztésének módja sokféle. Mutassunk egy példát a beillesztésre HTML attribútum segítségével. Legyen egy fotógalériánk, ahol a képekhez leírást lehet hozzáadni, amely az alt attribútumban íródik ki:

echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';

A támadónak elég leírásként egy ügyesen összeállított " onload="alert('Hacked!') stringet beilleszteni, és ha a kiírás nincs kezelve, az eredményül kapott kód így fog kinézni:

<img src="photo0145.webp" alt="" onload="alert('Hacked!')">

Az oldal részévé válik egy becsempészett onload attribútum. A böngésző a benne lévő kódot azonnal lefuttatja a kép letöltése után. Hacked!

Hogyan védekezzünk az XSS ellen?

Bármilyen kísérlet a támadás észlelésére feketelista segítségével, mint például a <script> string blokkolása stb., nem elegendő. A működő védelem alapja az oldalon belül kiírt összes adat következetes tisztítása.

Elsősorban arról van szó, hogy a speciális jelentéssel bíró összes karaktert helyettesíteni kell a megfelelő szekvenciákkal, amit szlengesen escapelésnek nevezünk (a szekvencia első karakterét escape karakternek nevezik, innen a név). Például a HTML szövegben a < karakternek speciális jelentése van, amelyet ha nem tag kezdeteként kell értelmezni, akkor helyettesíteni kell egy vizuálisan megfelelő szekvenciával, az ún. HTML entitással &lt;. És a böngésző kiír egy kisebb jelet.

Nagyon fontos megkülönböztetni a kontextust, amelyben az adatokat kiírjuk. Mivel különböző kontextusokban a stringek különbözőképpen tisztulnak. Különböző kontextusokban különböző karaktereknek van speciális jelentése. Például eltér az escapelés a HTML szövegben, a HTML attribútumokban, néhány speciális elemen belül stb. Hamarosan részletesen tárgyaljuk.

A kezelést a legjobb közvetlenül a string kiírásakor elvégezni az oldalon, ezzel biztosítva, hogy valóban megtörténik és pontosan egyszer történik meg. A legjobb, ha a kezelést automatikusan maga a sablonrendszer végzi. Mert ha a kezelés nem automatikus, a programozó elfelejtheti. És egyetlen mulasztás azt jelenti, hogy a weboldal sebezhető.

Azonban az XSS nemcsak az adatok sablonokban történő kiírását érinti, hanem az alkalmazás más részeit is, amelyeknek helyesen kell kezelniük a nem megbízható adatokat. Például szükséges, hogy az alkalmazás JavaScriptje ne használja az innerHTML-t velük kapcsolatban, hanem csak az innerText-et vagy textContent-et. Különös figyelmet kell fordítani azokra a függvényekre, amelyek stringeket JavaScriptként értékelnek ki, ami az eval(), de a setTimeout() is, vagy a setAttribute() függvény használata esemény attribútumokkal, mint az onload stb. Ez azonban már túlmutat a sablonok által lefedett területen.

Az ideális védelem 3 pontban:

  1. felismeri a kontextust, amelyben az adatok kiíródnak
  2. tisztítja az adatokat az adott kontextus szabályai szerint (tehát „kontextusérzékenyen”)
  3. ezt automatikusan teszi

Kontextusérzékeny escapelés

Mit jelent pontosan a kontextus szó? Ez egy hely a dokumentumban, saját szabályokkal a kiírt adatok kezelésére. A dokumentum típusától (HTML, XML, CSS, JavaScript, plain text, …) függ, és eltérhet annak konkrét részeiben. Például egy HTML dokumentumban számos ilyen hely (kontextus) van, ahol nagyon eltérő szabályok érvényesek. Talán meglepődik, mennyi van belőlük. Íme az első négy:

<p>#szöveg</p>
<img src="#attribútum">
<textarea>#rawtext</textarea>
<!-- #kommentár -->

A HTML oldal alapértelmezett és alapvető kontextusa a HTML szöveg. Milyen szabályok érvényesek itt? A < és & karaktereknek speciális jelentése van, amelyek a tag vagy entitás kezdetét jelentik, ezért escapelni kell őket, mégpedig HTML entitással való helyettesítéssel (< helyett &lt;, & helyett &amp;).

A második leggyakoribb kontextus a HTML attribútum értéke. A szövegtől abban különbözik, hogy itt a " vagy ' idézőjelnek van speciális jelentése, amely az attribútumot határolja. Ezt entitással kell írni, hogy ne az attribútum végeként értelmeződjön. Ellenben az attribútumban biztonságosan használható a < karakter, mert itt nincs speciális jelentése, itt nem értelmezhető tag vagy kommentár kezdeteként. De vigyázat, a HTML-ben az attribútumok értékeit idézőjelek nélkül is lehet írni, ebben az esetben számos karakternek van speciális jelentése, tehát ez egy további önálló kontextus.

Talán meglepő, de speciális szabályok érvényesek a <textarea> és <title> elemeken belül, ahol a < karaktert nem kell (de lehet) escapelni, ha nem követi /. De ez inkább csak érdekesség.

Érdekes a helyzet a HTML kommentárokon belül. Itt ugyanis nem használnak HTML entitásokat az escapeléshez. Sőt, egyetlen specifikáció sem írja le, hogyan kellene a kommentárokban escapelni. Csak be kell tartani néhány furcsa szabályokat és kerülni kell bennük bizonyos karakterkombinációkat.

A kontextusok rétegződhetnek is, ami akkor következik be, amikor JavaScriptet vagy CSS-t illesztünk be HTML-be. Ezt két különböző módon lehet megtenni, elemmel és attribútummal:

<script>#js-elem</script>
<img onclick="#js-attribútum">

<style>#css-elem</style>
<p style="#css-attribútum"></p>

Két út és két különböző adat escapelési mód. A <script> és <style> elemeken belül, ugyanúgy, mint a HTML kommentárok esetében, nem történik escapelés HTML entitásokkal. Az adatok kiírásakor ezeken az elemeken belül egyetlen szabályt kell betartani: a szöveg nem tartalmazhatja a </script ill. </style szekvenciát.

Ellenben a style és on*** attribútumokban HTML entitásokkal escapelünk.

És természetesen a beágyazott JavaScripten vagy CSS-en belül érvényesek ezeknek a nyelveknek az escapelési szabályai. Tehát egy string például az onload attribútumban először a JS szabályai szerint, majd a HTML attribútum szabályai szerint kerül escapelésre.

Hűha… Ahogy látja, a HTML egy nagyon komplex dokumentum, ahol a kontextusok rétegződnek, és anélkül, hogy tudatában lennénk annak, hol pontosan írjuk ki az adatokat (azaz milyen kontextusban), nem lehet megmondani, hogyan kell azt helyesen csinálni.

Szeretne egy példát?

Vegyünk egy Rock'n'Roll stringet.

Ha HTML szövegben írja ki, ebben az esetben nincs szükség semmilyen cserére, mert a string nem tartalmaz semmilyen speciális jelentéssel bíró karaktert. Más a helyzet, ha egy aposztrófokkal határolt HTML attribútumon belül írja ki. Ebben az esetben az aposztrófokat HTML entitásokra kell escapelni:

<div title='Rock&apos;n&apos;Roll'></div>

Ez egyszerű volt. Sokkal érdekesebb helyzet áll elő a kontextusok rétegződésekor, például ha a string egy JavaScript részévé válik.

Először tehát írjuk ki magába a JavaScriptbe. Azaz idézőjelek közé tesszük, és egyúttal escapeljük a benne lévő idézőjeleket a \ karakterrel:

'Rock\'n\'Roll'

Még hozzáadhatunk egy függvényhívást is, hogy a kód csináljon valamit:

alert('Rock\'n\'Roll');

Ha ezt a kódot a <script> segítségével illesztjük be a HTML dokumentumba, nincs szükség további módosításra, mert nem tartalmazza a tiltott </script szekvenciát:

<script> alert('Rock\'n\'Roll'); </script>

Ha azonban HTML attribútumba szeretnénk beilleszteni, még escapelnünk kell az idézőjeleket HTML entitásokra:

<div onclick='alert(&apos;Rock\&apos;n\&apos;Roll&apos;)'></div>

De a beágyazott kontextus nemcsak JS vagy CSS lehet. Gyakran URL is. Az URL paramétereket úgy escapeljük, hogy a speciális jelentéssel bíró karaktereket %-kal kezdődő szekvenciákra alakítjuk át. Példa:

https://example.org/?a=Jazz&b=Rock%27n%27Roll

És amikor ezt a stringet egy attribútumban írjuk ki, még alkalmazzuk az escapelést ennek a kontextusnak megfelelően, és helyettesítjük az &-t &amp-pal:

<a href="https://example.org/?a=Jazz&amp;b=Rock%27n%27Roll">

Ha idáig elolvasta, gratulálunk, kimerítő volt. Most már jó elképzelése van arról, mik azok a kontextusok és az escapelés. És nem kell aggódnia, hogy bonyolult. A Latte ezt ugyanis automatikusan megteszi Ön helyett.

Latte vs naiv rendszerek

Megmutattuk, hogyan kell helyesen escapelni egy HTML dokumentumban, és mennyire alapvető a kontextus ismerete, azaz annak a helynek az ismerete, ahol az adatokat kiírjuk. Más szóval, hogyan működik a kontextusérzékeny escapelés. Bár ez elengedhetetlen feltétele a működő XSS elleni védelemnek, a Latte az egyetlen PHP sablonrendszer, amely ezt tudja.

Hogyan lehetséges ez, amikor ma minden rendszer azt állítja, hogy automatikus escapeléssel rendelkezik? Az automatikus escapelés kontextus ismerete nélkül egy kicsit bullshit, amely a biztonság hamis érzetét kelti.

Az olyan sablonrendszerek, mint a Twig, a Laravel Blade és mások, nem látnak semmilyen HTML struktúrát a sablonban. Így nem látják a kontextusokat sem. A Latte-val ellentétben vakok és naivak. Csak a saját tagjeiket dolgozzák fel, minden más számukra lényegtelen karakterfolyam:

░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░
░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░
- szövegben: <span>{{ foo }}</span>
- tagben: <span {{ foo }} ></span>
- attribútumban: <span title='{{ foo }}'></span>
- attribútumban idézőjelek nélkül: <span title={{ foo }}></span>
- URL-t tartalmazó attribútumban: <a href="{{ foo }}"></a>
- JavaScriptet tartalmazó attribútumban: <img onload="{{ foo }}">
- CSS-t tartalmazó attribútumban: <span style="{{ foo }}"></span>
- JavaScriptben: <script>var = {{ foo }}</script>
- CSS-ben: <style>body { content: {{ foo }}; }</style>
- kommentárban: <!-- {{ foo }} -->

A naiv rendszerek csak mechanikusan alakítják át a < > & ' " karaktereket HTML entitásokká, ami ugyan a legtöbb felhasználási esetben érvényes escapelési mód, de korántsem mindig. Így nem tudják felfedezni vagy megelőzni a különböző biztonsági rések kialakulását, ahogy azt később bemutatjuk.

A Latte ugyanúgy látja a sablont, mint Ön. Érti a HTML-t, XML-t, felismeri a tageket, attribútumokat stb. És ennek köszönhetően megkülönbözteti az egyes kontextusokat és azok szerint kezeli az adatokat. Így valóban hatékony védelmet nyújt a kritikus Cross-site Scripting sebezhetőség ellen.

░░░░░░░░░░░<span>{$foo}</span>
░░░░░░░░░░<span {$foo} ></span>
░░░░░░░░░░░░░░<span title='{$foo}'></span>
░░░░░░░░░░░░░░░░░░░░░░░░░░░<span title={$foo}></span>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<a href="{$foo}"></a>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<img onload="{$foo}">
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<span style="{$foo}"></span>
░░░░░░░░░░░░░░░░░<script>░░░░░░{$foo}</script>
░░░░░░░░░<style>░░░░░░░░░░░░░░░░{$foo}░░░</style>
░░░░░░░░░░░░░░░<!--░{$foo}░-->
- szövegben: <span>{$foo}</span>
- tagben: <span {$foo} ></span>
- attribútumban: <span title='{$foo}'></span>
- attribútumban idézőjelek nélkül: <span title={$foo}></span>
- URL-t tartalmazó attribútumban: <a href="{$foo}"></a>
- JavaScriptet tartalmazó attribútumban: <img onload="{$foo}">
- CSS-t tartalmazó attribútumban: <span style="{$foo}"></span>
- JavaScriptben: <script>var = {$foo}</script>
- CSS-ben: <style>body { content: {$foo}; }</style>
- kommentárban: <!-- {$foo} -->

Élő bemutató

Bal oldalon a Latte sablont látja, jobb oldalon a generált HTML kódot. A $text változó többször kiíródik, és minden alkalommal egy kicsit más kontextusban. És ezért egy kicsit másképp escapelve. A sablon kódját Ön is szerkesztheti, például megváltoztathatja a változó tartalmát stb. Próbálja ki:

{* PRÓBÁLJA MEGSZERKESZTENI EZT A SABLONT *}
{var $text = "Rock'n'Roll"}
- <span>{$text}</span>
- <span title='{$text}'></span>
- <span title={$text}></span>
- <img onload="{$text}">
- <script>var = {$text}</script>
- <!-- {$text} -->
- <span>Rock'n'Roll</span>
- <span title='Rock&apos;n&apos;Roll'></span>
- <span title="Rock&apos;n&apos;Roll"></span>
- <img onload="&quot;Rock&apos;n&apos;Roll&quot;">
- <script>var = "Rock'n'Roll"</script>
- <!-- Rock'n'Roll -->

Hát nem nagyszerű! A Latte automatikusan végzi a kontextusérzékeny escapelést, így a programozó:

  • nem kell gondolkodnia vagy tudnia, hol hogyan kell escapelni
  • nem tévedhet
  • nem felejtheti el az escapelést

Ez még nem is az összes kontextus, amelyet a Latte megkülönböztet a kiíráskor, és amelyekhez igazítja az adatkezelést. További érdekes eseteket fogunk most áttekinteni.

Hogyan törjünk fel naiv rendszereket

Néhány gyakorlati példán keresztül bemutatjuk, mennyire fontos a kontextusok megkülönböztetése, és miért nem nyújtanak a naiv sablonrendszerek elegendő védelmet az XSS ellen, ellentétben a Latte-val. A naiv rendszer képviselőjeként a Twig-et használjuk a példákban, de ugyanez érvényes a többi rendszerre is.

Sebezhetőség attribútummal

Megpróbálunk rosszindulatú kódot injektálni az oldalba HTML attribútum segítségével, ahogy azt fentebb mutattuk. Legyen egy Twig sablonunk, amely egy képet jelenít meg:

<img src={{ imageFile }} alt={{ imageAlt }}>

Figyelje meg, hogy az attribútumok értékei körül nincsenek idézőjelek. A kódoló elfelejthette őket, ami egyszerűen előfordul. Például a Reactban a kódot így írják, idézőjelek nélkül, és a nyelveket váltogató kódoló könnyen elfelejtheti az idézőjeleket.

A támadó a kép leírásaként egy ügyesen összeállított foo onload=alert('Hacked!') stringet illeszt be. Már tudjuk, hogy a Twig nem tudja megállapítani, hogy a változó a HTML szövegfolyamban, egy attribútumon belül, HTML kommentárban stb. íródik-e ki, röviden nem különbözteti meg a kontextusokat. És csak mechanikusan alakítja át a < > & ' " karaktereket HTML entitásokká. Így az eredményül kapott kód így fog kinézni:

<img src=photo0145.webp alt=foo onload=alert(&#039;Hacked!&#039;)>

És létrejött egy biztonsági rés!

Az oldal részévé vált egy becsempészett onload attribútum, és a böngésző azonnal a kép letöltése után lefuttatja.

Most nézzük meg, hogyan bánik el ugyanezzel a sablonnal a Latte:

<img src={$imageFile} alt={$imageAlt}>

A Latte ugyanúgy látja a sablont, mint Ön. A Twiggel ellentétben érti a HTML-t, és tudja, hogy a változó egy attribútum értékeként íródik ki, amely nincs idézőjelek között. Ezért kiegészíti őket. Ha a támadó ugyanazt a leírást illeszti be, az eredményül kapott kód így fog kinézni:

<img src="photo0145.webp" alt="foo onload=alert(&apos;Hacked!&apos;)">

A Latte sikeresen megakadályozta az XSS-t.

Változó kiírása JavaScriptben

A kontextusérzékeny escapelésnek köszönhetően teljesen natívan lehet használni a PHP változókat JavaScripten belül.

<p onclick="alert({$movie})">{$movie}</p>

<script>var movie = {$movie};</script>

Ha a $movie változó az 'Amarcord & 8 1/2' stringet tartalmazza, a következő kimenet generálódik. Figyelje meg, hogy a HTML-en belül más escapelés használatos, mint a JavaScripten belül, és megint más az onclick attribútumban:

<p onclick="alert(&quot;Amarcord &amp; 8 1\/2&quot;)">Amarcord &amp; 8 1/2</p>

<script>var movie = "Amarcord & 8 1\/2";</script>

Linkek ellenőrzése

A Latte automatikusan ellenőrzi, hogy a src vagy href attribútumokban használt változó webes URL-t tartalmaz-e (azaz HTTP protokollt), és megakadályozza az olyan linkek kiírását, amelyek biztonsági kockázatot jelenthetnek.

{var $link = 'javascript:attack()'}

<a href={$link}>kattints</a>

Kiírja:

<a href="">kattints</a>

Az ellenőrzést a nocheck szűrővel lehet kikapcsolni.

A Latte korlátai

A Latte nem nyújt teljesen teljes körű védelmet az XSS ellen az egész alkalmazás számára. Nem szeretnénk, ha a Latte használatakor abbahagyná a biztonságra való gondolkodást. A Latte célja annak biztosítása, hogy a támadó ne tudja megváltoztatni az oldal struktúráját, becsempészni HTML elemeket vagy attribútumokat. De nem ellenőrzi a kiírt adatok tartalmi helyességét. Vagy a JavaScript viselkedésének helyességét. Ez már túlmutat a sablonrendszer kompetenciáin. Az adatok helyességének ellenőrzése, különösen a felhasználó által bevitt és ezért nem megbízható adatoké, a programozó fontos feladata.

verzió: 3.0