Latte je sinonim za varnost

Latte je edini sistem predlog za PHP z učinkovito zaščito pred kritično ranljivostjo Cross-site Scripting (XSS). In to zahvaljujoč t.i. kontekstno občutljivemu ubežanju. Povedali si bomo,

  • kakšen je princip ranljivosti XSS in zakaj je tako nevarna
  • zakaj je Latte pri obrambi pred XSS tako učinkovit
  • kako je mogoče v predlogah Twig, Blade in podobnih enostavno narediti varnostno luknjo

Cross-site Scripting (XSS)

Cross-site Scripting (skrajšano XSS) je ena najpogostejših ranljivosti spletnih strani in hkrati zelo nevarna. Napadalcu omogoča, da v tujo stran vstavi škodljiv skript (t.i. malware), ki se zažene v brskalniku nič hudega slutečega uporabnika.

Kaj vse lahko tak skript povzroči? Lahko na primer pošlje napadalcu poljubno vsebino z napadene strani, vključno z občutljivimi podatki, prikazanimi po prijavi. Lahko stran spremeni ali izvaja nadaljnje zahteve v imenu uporabnika. Če bi šlo na primer za spletno pošto, lahko prebere občutljiva sporočila, spremeni prikazano vsebino ali ponastavi konfiguracijo, npr. vklopi prepošiljanje kopij vseh sporočil na napadalčev naslov, da pridobi dostop tudi do prihodnjih e-poštnih sporočil.

Zato tudi XSS figurira na prvih mestih lestvic najnevarnejših ranljivosti. Če se na spletni strani pojavi ranljivost, jo je treba čim prej odstraniti, da se prepreči zloraba.

Kako nastane ranljivost?

Napaka nastane na mestu, kjer se spletna stran generira in izpisujejo spremenljivke. Predstavljajte si, da ustvarjate stran z iskanjem, in na začetku bo odstavek z iskanim izrazom v obliki:

echo '<p>Rezultati iskanja za <em>' . $search . '</em></p>';

Napadalec lahko v iskalno polje in posledično v spremenljivko $search zapiše poljuben niz, torej tudi HTML kodo kot <script>alert("Hacked!")</script>. Ker izpis ni nikakor obdelan, postane del prikazane strani:

<p>Rezultati iskanja za <em><script>alert("Hacked!")</script></em></p>

Brskalnik namesto da bi izpisal iskani niz, zažene JavaScript. In s tem prevzame nadzor nad stranjo napadalec.

Lahko ugovarjate, da z vstavitvijo kode v spremenljivko sicer pride do zagona JavaScripta, vendar samo v napadalčevem brskalniku. Kako pride do žrtve? S tega vidika ločimo več vrst XSS. V našem primeru z iskanjem govorimo o reflected XSS. Tukaj je še treba žrtev napeljati, da klikne na povezavo, ki bo vsebovala škodljivo kodo v parametru:

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

Napeljati uporabnika na povezavo sicer zahteva določeno socialno inženirstvo, vendar ni nič zapletenega. Uporabniki na povezave, bodisi v e-poštnih sporočilih ali na družbenih omrežjih, klikajo brez večjih premislekov. In da je v naslovu nekaj sumljivega, se da zamaskirati s pomočjo skrajševalca URL, uporabnik nato vidi samo bit.ly/xxx.

Vendar obstaja tudi druga in veliko nevarnejša oblika napada, označena kot stored XSS ali persistent XSS, ko napadalcu uspe shraniti škodljivo kodo na strežnik tako, da se samodejno vstavlja v nekatere strani.

Primer so strani, kamor uporabniki pišejo komentarje. Napadalec pošlje prispevek, ki vsebuje kodo, in ta se shrani na strežnik. Če strani niso dovolj zaščitene, se bo nato zagnal v brskalniku vsakega obiskovalca.

Morda se zdi, da je jedro napada v tem, da se v stran vnese niz <script>. V resnici načinov vstavljanja JavaScripta je veliko. Pokažimo si na primer vstavljanje s pomočjo atributa HTML. Imejmo fotogalerijo, kjer je mogoče slikam dodajati opis, ki se izpiše v atributu alt:

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

Napadalcu zadostuje, da kot opis vstavi spretno sestavljen niz " onload="alert('Hacked!') in če izpis ne bo obdelan, bo rezultatna koda izgledala takole:

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

Del strani zdaj postane podtaknjen atribut onload. Brskalnik kodo v njem vsebovano zažene takoj po prenosu slike. Hacked!

Kako se braniti pred XSS?

Kakršni koli poskusi zaznavanja napada s pomočjo črne liste, kot na primer blokiranje niza <script> ipd., so nezadostni. Osnova funkcionalne obrambe je dosledna sanacija vseh podatkov, izpisanih znotraj strani.

Predvsem gre za zamenjavo vseh znakov s posebnim pomenom z drugimi ustreznimi zaporedji, čemur se slengovsko reče ubežanje znakov (prvi znak zaporedja se imenuje ubežni znak, od tod poimenovanje). Na primer, v besedilu HTML ima poseben pomen znak <, ki ga, če ne sme biti interpretiran kot začetek oznake, moramo zamenjati z vizualno ustreznim zaporedjem, t.i. HTML entiteto &lt;. In brskalnik izpiše znak manjše kot.

Zelo pomembno je razlikovati kontekst, v katerem izpisujemo podatke. Ker se v različnih kontekstih nizi različno sanirajo. V različnih kontekstih imajo poseben pomen različni znaki. Na primer, razlikuje se ubežanje v besedilu HTML, v atributih HTML, znotraj nekaterih posebnih elementov, itd. Čez trenutek bomo to podrobneje obravnavali.

Obdelavo je najbolje izvesti neposredno ob izpisu niza na strani, s čimer zagotovimo, da se res izvede in izvede natanko enkrat. Najbolje je, če obdelavo zagotovi samodejno neposredno sistem predlog. Ker če obdelava ne poteka samodejno, lahko programer nanjo pozabi. In ena opustitev pomeni, da je splet ranljiv.

Vendar se XSS ne nanaša samo na izpisovanje podatkov v predlogah, ampak tudi na druge dele aplikacije, ki morajo pravilno ravnati z nezaupljivimi podatki. Na primer, nujno je, da JavaScript v vaši aplikaciji v povezavi z njimi ne uporablja innerHTML, ampak samo innerText ali textContent. Posebno pozornost je treba nameniti funkcijam, ki vrednotijo nize kot JavaScript, kar je eval(), pa tudi setTimeout(), ali uporabi funkcije setAttribute() z atributi dogodkov, kot je onload ipd. To pa že presega področje, ki ga pokrivajo predloge.

Idealna obramba v 3 točkah:

  1. prepozna kontekst, v katerem se podatki izpisujejo
  2. sanira podatke po pravilih danega konteksta (torej „kontekstno občutljivo“)
  3. dela to samodejno

Kontekstno občutljivo ubežanje

Kaj točno se misli z besedo kontekst? Gre za mesto v dokumentu z lastnimi pravili za obdelavo izpisanih podatkov. Odvisno je od vrste dokumenta (HTML, XML, CSS, JavaScript, navadno besedilo, …) in se lahko razlikuje v njegovih konkretnih delih. Na primer, v dokumentu HTML je takih mest (kontekstov), kjer veljajo zelo različna pravila, cela vrsta. Morda boste presenečeni, koliko jih je. Tukaj imamo prvo četverico:

<p>#text</p>
<img src="#atribut">
<textarea>#rawtext</textarea>
<!-- #komentar -->

Izhodiščni in osnovni kontekst strani HTML je besedilo HTML. Kakšna pravila tukaj veljajo? Poseben pomen imata znaka < in &, ki predstavljata začetek oznake ali entitete, zato ju moramo ubežati, in sicer z zamenjavo za HTML entiteto (< za &lt;, & za &amp).

Drugi najpogostejši kontekst je vrednost atributa HTML. Od besedila se razlikuje v tem, da ima tukaj poseben pomen narekovaj " ali ', ki atribut omejuje. Tega je treba zapisati z entiteto, da ne bi bil razumljen kot konec atributa. Nasprotno pa je v atributu mogoče varno uporabljati znak <, ker tukaj nima nobenega posebnega pomena, tukaj ne more biti razumljen kot začetek oznake ali komentarja. Ampak pozor, v HTML je mogoče pisati vrednosti atributov tudi brez narekovajev, v takem primeru ima poseben pomen cela vrsta znakov, gre torej za drug samostojen kontekst.

Morda vas bo presenetilo, ampak posebna pravila veljajo znotraj elementov <textarea> in <title>, kjer se znak < ni treba (lahko pa se) ubežati, če mu ne sledi /. Ampak to je bolj zanimivost.

Zanimivo je znotraj komentarjev HTML. Tukaj se namreč za ubežanje ne uporabljajo HTML entitete. Celo nobena specifikacija ne navaja, kako naj bi se v komentarjih ubežalo. Samo treba je upoštevati nekoliko nenavadna pravila in se v njih izogibati določenim kombinacijam znakov.

Konteksti se lahko tudi plastijo, do česar pride, ko vstavimo JavaScript ali CSS v HTML. To je mogoče storiti na dva različna načina, z elementom in atributom:

<script>#js-element</script>
<img onclick="#js-atribut">

<style>#css-element</style>
<p style="#css-atribut"></p>

Dve poti in dva različna načina ubežanja podatkov. Znotraj elementa <script> in <style> se, enako kot v primeru komentarjev HTML, ubežanje s pomočjo HTML entitet ne izvaja. Pri izpisovanju podatkov znotraj teh elementov je treba upoštevati edino pravilo: besedilo ne sme vsebovati zaporedja </script oz. </style.

Nasprotno pa se v atributih style in on*** ubeža s pomočjo HTML entitet.

In seveda znotraj vgnezdenega JavaScripta ali CSS veljajo pravila ubežanja teh jezikov. Torej se niz v atributu npr. onload najprej ubeža po pravilih JS in nato po pravilih atributa HTML.

Uff… Kot vidite, je HTML zelo kompleksen dokument, kjer se plastijo konteksti, in brez zavedanja, kje točno podatke izpisujem (tj. v kakšnem kontekstu), ni mogoče reči, kako to pravilno storiti.

Želite primer?

Imejmo niz Rock'n'Roll.

Če ga boste izpisovali v besedilu HTML, ravno v tem primeru ni treba delati nobenih zamenjav, ker niz ne vsebuje nobenega znaka s posebnim pomenom. Druga situacija nastane, če ga izpišete znotraj atributa HTML, omejenega z enojnimi narekovaji. V takem primeru je treba narekovaje ubežati v HTML entitete:

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

To je bilo preprosto. Veliko bolj zanimiva situacija nastane pri plastenju kontekstov, na primer, če bo niz del JavaScripta.

Najprej ga torej izpišemo v samem JavaScriptu. Tj. ovijemo ga v narekovaje in hkrati ubežamo z znakom \ narekovaje v njem vsebovane:

'Rock\'n\'Roll'

Še lahko dopolnimo klic neke funkcije, da koda nekaj počne:

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

Če to kodo vstavimo v dokument HTML s pomočjo <script>, ni treba ničesar dodatno urejati, ker se v njej ne pojavlja prepovedano zaporedje </script:

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

Če pa bi jo želeli vstaviti v atribut HTML, moramo še ubežati narekovaje v HTML entitete:

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

Vgnezden kontekst pa ni nujno samo JS ali CSS. Pogosto je to tudi URL. Parametri v URL se ubežajo tako, da se znaki s posebnim pomenom pretvorijo v zaporedja, ki se začnejo z %. Primer:

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

In ko ta niz izpišemo v atributu, še uporabimo ubežanje po tem kontekstu in zamenjamo & za &amp:

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

Če ste prebrali do sem, čestitamo, bilo je izčrpno. Zdaj imate dobro predstavo o tem, kaj so konteksti in ubežanje. In ni vam treba skrbeti, da je zapleteno. Latte to namreč dela za vas samodejno.

Latte proti naivnim sistemom

Pokazali smo si, kako se pravilno ubeža v dokumentu HTML in kako bistveno je poznavanje konteksta, torej mesta, kjer podatke izpisujemo. Z drugimi besedami, kako deluje kontekstno občutljivo ubežanje. Čeprav gre za nujen predpogoj funkcionalne obrambe pred XSS, je Latte edini sistem predlog za PHP, ki to zna.

Kako je to mogoče, ko pa vsi sistemi danes trdijo, da imajo samodejno ubežanje? Samodejno ubežanje brez poznavanja konteksta je malce bullshit, ki ustvarja lažen občutek varnosti.

Sistemi predlog, kot so Twig, Laravel Blade in drugi, v predlogi ne vidijo nobene strukture HTML. Zato ne vidijo niti kontekstov. V primerjavi z Latte so slepi in naivni. Obdelujejo samo lastne oznake, vse ostalo je zanje nepomemben tok znakov:

░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░
░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░
- v besedilu: <span>{{ foo }}</span>
- v oznaki: <span {{ foo }} ></span>
- v atributu: <span title='{{ foo }}'></span>
- v atributu brez narekovajev: <span title={{ foo }}></span>
- v atributu, ki vsebuje URL: <a href="{{ foo }}"></a>
- v atributu, ki vsebuje JavaScript: <img onload="{{ foo }}">
- v atributu, ki vsebuje CSS: <span style="{{ foo }}"></span>
- v JavaScriptu: <script>var = {{ foo }}</script>
- v CSS: <style>body { content: {{ foo }}; }</style>
- v komentarju: <!-- {{ foo }} -->

Naivni sistemi samo mehansko pretvarjajo znake < > & ' " v HTML entitete, kar je sicer v večini primerov uporabe veljaven način ubežanja, vendar še zdaleč ne vedno. Ne morejo tako odkriti niti preprečiti nastanka različnih varnostnih lukenj, kot bomo pokazali dalje.

Latte predlogo vidi enako kot vi. Razume HTML, XML, prepoznava oznake, atribute itd. In zahvaljujoč temu razlikuje posamezne kontekste in po njih obdeluje podatke. Ponuja tako resnično učinkovito zaščito pred kritično ranljivostjo Cross-site Scripting.

░░░░░░░░░░░<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}░-->
- v besedilu: <span>{$foo}</span>
- v oznaki: <span {$foo} ></span>
- v atributu: <span title='{$foo}'></span>
- v atributu brez narekovajev: <span title={$foo}></span>
- v atributu, ki vsebuje URL: <a href="{$foo}"></a>
- v atributu, ki vsebuje JavaScript: <img onload="{$foo}">
- v atributu, ki vsebuje CSS: <span style="{$foo}"></span>
- v JavaScriptu: <script>var = {$foo}</script>
- v CSS: <style>body { content: {$foo}; }</style>
- v komentarju: <!-- {$foo} -->

Primer v živo

Na levi vidite predlogo v Latte, na desni je generirana koda HTML. Večkrat se tu izpisuje spremenljivka $text in vsakič v malce drugačnem kontekstu. In torej tudi malce drugače ubežana. Kodo predloge lahko sami urejate, na primer spremenite vsebino spremenljivke itd. Poskusite si:

{* POSKUSITE UREDITI TO PREDLOGO *}
{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 -->

Ni to super! Latte dela kontekstno občutljivo ubežanje samodejno, tako da programer:

  • ni treba razmišljati niti vedeti, kako se kje ubeža
  • se ne more zmotiti
  • ne more pozabiti na ubežanje

To celo niso vsi konteksti, ki jih Latte pri izpisovanju razlikuje in za katere prilagaja obdelavo podatkov. Druge zanimive primere si bomo ogledali zdaj.

Kako vdreti v naivne sisteme

Na nekaj praktičnih primerih si bomo pokazali, kako pomembno je razlikovanje kontekstov in zakaj naivni sistemi predlog ne zagotavljajo zadostne zaščite pred XSS, za razliko od Latte. Kot predstavnika naivnega sistema bomo v primerih uporabili Twig, vendar isto velja tudi za druge sisteme.

Ranljivost atributa

Poskusili bomo v stran injicirati škodljivo kodo s pomočjo atributa HTML, kot smo si prikazali zgoraj. Imejmo predlogo v Twigu, ki izrisuje sliko:

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

Opazite, da okoli vrednosti atributov ni narekovajev. Koder jih je lahko pozabil, kar se pač dogaja. Na primer v Reactu se koda piše tako, brez narekovajev, in koder, ki menja jezike, lahko nato na narekovaje zlahka pozabi.

Napadalec kot opis slike vstavi spretno sestavljen niz foo onload=alert('Hacked!'). Že vemo, da Twig ne more prepoznati, ali se spremenljivka izpisuje v toku besedila HTML, znotraj atributa, komentarja HTML, itd., skratka ne razlikuje kontekstov. In samo mehansko pretvarja znake < > & ' " v HTML entitete. Torej bo rezultatna koda izgledala takole:

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

In nastala je varnostna luknja!

Del strani je postal podtaknjen atribut onload in brskalnik ga takoj po prenosu slike zažene.

Zdaj si poglejmo, kako se z enako predlogo spopade Latte:

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

Latte vidi predlogo enako kot vi. Za razliko od Twiga razume HTML in ve, da se spremenljivka izpisuje kot vrednost atributa, ki ni v narekovajih. Zato jih dopolni. Ko napadalec vstavi enak opis, bo rezultatna koda izgledala takole:

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

Latte je uspešno preprečil XSS.

Izpis spremenljivke v JavaScriptu

Zahvaljujoč kontekstno občutljivemu ubežanju je mogoče popolnoma izvorno uporabljati spremenljivke PHP znotraj JavaScripta.

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

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

Če bo spremenljivka $movie vsebovala niz 'Amarcord & 8 1/2', se bo generiral naslednji izpis. Opazite, da se znotraj HTML uporabi drugačno ubežanje kot znotraj JavaScripta in še drugačno v atributu onclick:

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

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

Preverjanje povezav

Latte samodejno preverja, ali spremenljivka, uporabljena v atributih src ali href, vsebuje spletni URL (tj. protokol HTTP) in preprečuje izpis povezav, ki lahko predstavljajo varnostno tveganje.

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

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

Izpiše:

<a href="">klikni</a>

Preverjanje se da izklopiti s pomočjo filtra nocheck.

Omejitve Latte

Latte ni popolnoma celovita zaščita pred XSS za celotno aplikacijo. Ne bi želeli, da bi ob uporabi Latte prenehali razmišljati o varnosti. Cilj Latte je zagotoviti, da napadalec ne more spremeniti strukture strani, podtakniti elementov HTML ali atributov. Vendar ne preverja vsebinske pravilnosti izpisanih podatkov. Ali pravilnosti delovanja JavaScripta. To že presega pristojnosti sistema predlog. Preverjanje pravilnosti podatkov, zlasti tistih, ki jih vnese uporabnik in so torej nezaupljivi, je pomembna naloga programerja.

različica: 3.0