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
<
. É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:
- felismeri a kontextust, amelyben az adatok kiíródnak
- tisztítja az adatokat az adott kontextus szabályai szerint (tehát „kontextusérzékenyen”)
- 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 <
,
&
helyett &
).
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'n'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('Rock\'n\'Roll')'></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 &
-pal:
<a href="https://example.org/?a=Jazz&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:
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('Hacked!')>
É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('Hacked!')">
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("Amarcord & 8 1\/2")">Amarcord & 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.