Latte este sinonim cu securitatea
Latte este singurul sistem de șabloane pentru PHP cu protecție eficientă împotriva vulnerabilității critice Cross-site Scripting (XSS). Și asta datorită așa-numitei escapări contextuale sensibile. Vom discuta despre:
- care este principiul vulnerabilității XSS și de ce este atât de periculoasă
- de ce este Latte atât de eficient în apărarea împotriva XSS
- cum se poate crea ușor o gaură de securitate în șabloanele Twig, Blade și altele
Cross-site Scripting (XSS)
Cross-site Scripting (prescurtat XSS) este una dintre cele mai frecvente vulnerabilități ale site-urilor web și, în același timp, foarte periculoasă. Permite unui atacator să insereze un script malițios (așa-numitul malware) într-o pagină străină, care se execută în browserul unui utilizator nebănuitor.
Ce poate face un astfel de script? Poate, de exemplu, să trimită atacatorului orice conținut din pagina atacată, inclusiv date sensibile afișate după autentificare. Poate modifica pagina sau efectua alte cereri în numele utilizatorului. Dacă ar fi, de exemplu, un webmail, ar putea citi mesaje sensibile, modifica conținutul afișat sau reconfigura setările, de ex. să activeze redirecționarea copiilor tuturor mesajelor către adresa atacatorului, pentru a obține acces și la emailurile viitoare.
De aceea, XSS figurează pe primele locuri în clasamentele celor mai periculoase vulnerabilități. Dacă apare o vulnerabilitate pe un site web, este necesar să fie eliminată cât mai curând posibil pentru a preveni abuzul.
Cum apare vulnerabilitatea?
Eroarea apare în locul unde pagina web este generată și variabilele sunt afișate. Imaginați-vă că creați o pagină cu căutare, iar la început va fi un paragraf cu termenul căutat sub forma:
echo '<p>Rezultatele căutării pentru <em>' . $search . '</em></p>';
Un atacator poate introduce în câmpul de căutare și, implicit, în variabila $search
, orice șir, deci și cod
HTML precum <script>alert("Hacked!")</script>
. Deoarece ieșirea nu este tratată în niciun fel, aceasta
devine parte a paginii afișate:
<p>Rezultatele căutării pentru <em><script>alert("Hacked!")</script></em></p>
Browserul, în loc să afișeze șirul căutat, execută JavaScript. Și astfel, atacatorul preia controlul asupra paginii.
Puteți obiecta că, deși introducerea codului într-o variabilă duce la executarea JavaScriptului, acest lucru se întâmplă doar în browserul atacatorului. Cum ajunge la victimă? Din acest punct de vedere, distingem mai multe tipuri de XSS. În exemplul nostru cu căutarea, vorbim despre reflected XSS. Aici, este încă necesar să convingem victima să facă clic pe un link care va conține codul malițios în parametru:
https://example.com/?search=<script>alert("Hacked!")</script>
Convingerea utilizatorului să acceseze linkul necesită, desigur, o anumită inginerie socială, dar nu este nimic complicat.
Utilizatorii fac clic pe linkuri, fie în emailuri, fie pe rețelele sociale, fără prea multă gândire. Iar faptul că adresa
conține ceva suspect poate fi mascat folosind un scurtător de URL, utilizatorul văzând apoi doar bit.ly/xxx
.
Cu toate acestea, există și o a doua formă de atac, mult mai periculoasă, denumită stored XSS sau persistent XSS, în care atacatorul reușește să salveze codul malițios pe server astfel încât să fie inserat automat în unele pagini.
Un exemplu sunt paginile unde utilizatorii scriu comentarii. Atacatorul trimite o postare care conține cod, iar acesta este salvat pe server. Dacă paginile nu sunt suficient securizate, acesta se va executa apoi în browserul fiecărui vizitator.
S-ar putea părea că nucleul atacului constă în introducerea șirului <script>
în pagină. În
realitate, există multe modalități
de a insera JavaScript. Să arătăm un exemplu de inserare folosind un atribut HTML. Să avem o galerie foto unde se pot
adăuga descrieri la imagini, care se afișează în atributul alt
:
echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';
Atacatorului îi este suficient să introducă ca descriere un șir inteligent construit
" onload="alert('Hacked!')
și dacă afișarea nu este tratată, codul rezultat va arăta astfel:
<img src="photo0145.webp" alt="" onload="alert('Hacked!')">
Un atribut onload
falsificat devine acum parte a paginii. Browserul execută codul conținut în el imediat după
descărcarea imaginii. Hacked!
Cum să ne apărăm de XSS?
Orice încercare de a detecta atacul folosind o listă neagră, cum ar fi blocarea șirului <script>
etc.,
este insuficientă. Baza unei apărări funcționale este sanitizarea consecventă a tuturor datelor afișate în interiorul
paginii.
În principal, este vorba despre înlocuirea tuturor caracterelor cu semnificație specială cu alte secvențe
corespunzătoare, ceea ce se numește în jargon escapare (primul caracter al secvenței se numește caracter de evadare,
de unde și denumirea). De exemplu, în textul HTML, caracterul <
are o semnificație specială, iar dacă nu
trebuie interpretat ca începutul unui tag, trebuie să îl înlocuim cu o secvență vizual corespunzătoare, așa-numita
entitate HTML <
. Și browserul va afișa semnul mai mic.
Este foarte important să distingem contextul în care afișăm datele. Deoarece în contexte diferite, șirurile se sanitizează diferit. În contexte diferite, caractere diferite au semnificație specială. De exemplu, escaparea diferă în textul HTML, în atributele HTML, în interiorul unor elemente speciale etc. Vom discuta acest lucru în detaliu în curând.
Tratarea este cel mai bine efectuată direct la afișarea șirului în pagină, asigurându-ne astfel că se realizează cu adevărat și se realizează exact o dată. Cel mai bine este dacă tratarea este asigurată automat direct de sistemul de șabloane. Deoarece dacă tratarea nu are loc automat, programatorul poate uita de ea. Și o singură omisiune înseamnă că site-ul este vulnerabil.
Cu toate acestea, XSS nu se referă doar la afișarea datelor în șabloane, ci și la alte părți ale aplicației care
trebuie să gestioneze corect datele nesigure. De exemplu, este necesar ca JavaScriptul din aplicația dvs. să nu utilizeze
innerHTML
în legătură cu acestea, ci doar innerText
sau textContent
. O atenție
specială trebuie acordată funcțiilor care evaluează șirurile ca JavaScript, cum ar fi eval()
, dar și
setTimeout()
, sau utilizarea funcției setAttribute()
cu atribute de eveniment precum
onload
etc. Acest lucru depășește însă domeniul acoperit de șabloane.
Apărarea ideală în 3 puncte:
- recunoaște contextul în care se afișează datele
- sanitizează datele conform regulilor contextului dat (adică „contextual sensibil”)
- face acest lucru automat
Escapare contextuală sensibilă
Ce se înțelege exact prin cuvântul context? Este un loc în document cu propriile reguli pentru tratarea datelor afișate. Depinde de tipul documentului (HTML, XML, CSS, JavaScript, plain text, …) și poate diferi în părțile sale specifice. De exemplu, într-un document HTML există o serie întreagă de astfel de locuri (contexte) unde se aplică reguli foarte diferite. Poate veți fi surprinși câte sunt. Iată primele patru:
<p>#text</p>
<img src="#atribut">
<textarea>#rawtext</textarea>
<!-- #comentariu -->
Contextul implicit și de bază al unei pagini HTML este textul HTML. Care sunt regulile aici? Caracterele <
și &
au o semnificație specială, reprezentând începutul unui tag sau al unei entități, deci trebuie să
le escapăm, înlocuindu-le cu entități HTML (<
cu <
, &
cu
&
).
Al doilea cel mai comun context este valoarea unui atribut HTML. Diferă de text prin faptul că ghilimelele "
sau
'
, care delimitează atributul, au aici o semnificație specială. Acestea trebuie scrise ca entități pentru a nu
fi înțelese ca sfârșitul atributului. În schimb, în atribut se poate folosi în siguranță caracterul <
,
deoarece aici nu are nicio semnificație specială, nu poate fi înțeles ca începutul unui tag sau comentariu. Dar atenție, în
HTML se pot scrie valorile atributelor și fără ghilimele, caz în care o întreagă serie de caractere au semnificație
specială, deci este vorba despre un alt context separat.
Poate vă va surprinde, dar reguli speciale se aplică în interiorul elementelor <textarea>
și
<title>
, unde caracterul <
nu trebuie (dar poate) escapat, dacă nu este urmat de
/
. Dar aceasta este mai mult o curiozitate.
Interesant este în interiorul comentariilor HTML. Aici, escaparea nu se face folosind entități HTML. De fapt, nicio specificație nu indică cum ar trebui să se escapeze în comentarii. Este necesar doar să se respecte niște reguli oarecum curioase și să se evite anumite combinații de caractere în ele.
Contexturile se pot și stratifica, ceea ce se întâmplă când inserăm JavaScript sau CSS în HTML. Acest lucru se poate face în două moduri diferite, prin element și atribut:
<script>#js-element</script>
<img onclick="#js-atribut">
<style>#css-element</style>
<p style="#css-atribut"></p>
Două căi și două moduri diferite de escapare a datelor. În interiorul elementelor <script>
și
<style>
, la fel ca în cazul comentariilor HTML, escaparea folosind entități HTML nu se efectuează. La
afișarea datelor în interiorul acestor elemente, trebuie respectată o singură regulă: textul nu trebuie să conțină
secvența </script
respectiv </style
.
În schimb, în atributele style
și on***
se escapează folosind entități HTML.
Și, desigur, în interiorul JavaScriptului sau CSS-ului imbricat se aplică regulile de escapare ale acestor limbaje. Deci, un
șir într-un atribut, de ex. onload
, se escapează mai întâi conform regulilor JS și apoi conform regulilor
atributului HTML.
Uff… După cum vedeți, HTML este un document foarte complex, unde contextele se stratifică, și fără a conștientiza unde exact afișez datele (adică în ce context), nu se poate spune cum să o facem corect.
Doriți un exemplu?
Să avem șirul Rock'n'Roll
.
Dacă îl veți afișa în text HTML, în acest caz particular nu este nevoie de nicio înlocuire, deoarece șirul nu conține niciun caracter cu semnificație specială. Situația se schimbă dacă îl afișați în interiorul unui atribut HTML delimitat de apostrofuri simple. În acest caz, este necesar să escapați apostrofurile în entități HTML:
<div title='Rock'n'Roll'></div>
Acesta a fost simplu. O situație mult mai interesantă apare la stratificarea contextelor, de exemplu, dacă șirul va face parte din JavaScript.
Mai întâi, îl vom afișa în JavaScript în sine. Adică, îl vom încadra în ghilimele și, în același timp, vom escapa
folosind caracterul \
ghilimelele conținute în el:
'Rock\'n\'Roll'
Mai putem adăuga apelul unei funcții, pentru ca codul să facă ceva:
alert('Rock\'n\'Roll');
Dacă inserăm acest cod într-un document HTML folosind <script>
, nu mai este nevoie de nicio altă
modificare, deoarece nu conține secvența interzisă </script
:
<script> alert('Rock\'n\'Roll'); </script>
Dacă am dori însă să îl inserăm într-un atribut HTML, trebuie să mai escapăm ghilimelele în entități HTML:
<div onclick='alert('Rock\'n\'Roll')'></div>
Dar contextul imbricat nu trebuie să fie doar JS sau CSS. Adesea, acesta este și URL-ul. Parametrii din URL se escapează
prin convertirea caracterelor cu semnificație specială în secvențe care încep cu %
. Exemplu:
https://example.org/?a=Jazz&b=Rock%27n%27Roll
Și când afișăm acest șir într-un atribut, aplicăm și escaparea conform acestui context și înlocuim &
cu &
:
<a href="https://example.org/?a=Jazz&b=Rock%27n%27Roll">
Dacă ați citit până aici, felicitări, a fost epuizant. Acum aveți o idee bună despre ce sunt contextele și escaparea. Și nu trebuie să vă faceți griji că este complicat. Latte face acest lucru automat pentru dvs.
Latte vs sisteme naive
Am arătat cum se escapează corect într-un document HTML și cât de esențială este cunoașterea contextului, adică a locului unde afișăm datele. Cu alte cuvinte, cum funcționează escaparea contextuală sensibilă. Deși este o condiție necesară pentru o apărare funcțională împotriva XSS, Latte este singurul sistem de șabloane pentru PHP care știe să facă acest lucru.
Cum este posibil, când toate sistemele pretind astăzi că au escapare automată? Escaparea automată fără cunoașterea contextului este un pic de bullshit, care creează o falsă impresie de securitate.
Sistemele de șabloane, cum ar fi Twig, Laravel Blade și altele, nu văd nicio structură HTML în șablon. Prin urmare, nu văd nici contextele. Spre deosebire de Latte, sunt oarbe și naive. Procesează doar propriile tag-uri, tot restul este pentru ele un flux nesemnificativ de caractere:
░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░
░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░
- în text: <span>{{ foo }}</span>
- în tag: <span {{ foo }} ></span>
- în atribut: <span title='{{ foo }}'></span>
- în atribut fără ghilimele: <span title={{ foo }}></span>
- în atribut conținând URL: <a href="{{ foo }}"></a>
- în atribut conținând JavaScript: <img onload="{{ foo }}">
- în atribut conținând CSS: <span style="{{ foo }}"></span>
- în JavaScript: <script>var = {{ foo }}</script>
- în CSS: <style>body { content: {{ foo }}; }</style>
- în comentariu: <!-- {{ foo }} -->
Sistemele naive doar convertesc mecanic caracterele < > & ' "
în entități HTML, ceea ce, deși este
o metodă validă de escapare în majoritatea cazurilor de utilizare, nu este nici pe departe întotdeauna așa. Nu pot astfel
detecta sau preveni apariția diverselor găuri de securitate, așa cum vom arăta în continuare.
Latte vede șablonul la fel ca dvs. Înțelege HTML, XML, recunoaște tag-uri, atribute etc. Și datorită acestui fapt, distinge contextele individuale și tratează datele conform acestora. Oferă astfel o protecție cu adevărat eficientă împotriva vulnerabilității critice 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}░-->
- în text: <span>{$foo}</span>
- în tag: <span {$foo} ></span>
- în atribut: <span title='{$foo}'></span>
- în atribut fără ghilimele: <span title={$foo}></span>
- în atribut conținând URL: <a href="{$foo}"></a>
- în atribut conținând JavaScript: <img onload="{$foo}">
- în atribut conținând CSS: <span style="{$foo}"></span>
- în JavaScript: <script>var = {$foo}</script>
- în CSS: <style>body { content: {$foo}; }</style>
- în comentariu: <!-- {$foo} -->
Demonstrație live
În stânga vedeți șablonul în Latte, în dreapta este codul HTML generat. Variabila $text
este afișată de
mai multe ori aici, și de fiecare dată într-un context ușor diferit. Și, prin urmare, și escapată ușor diferit. Puteți
edita singur codul șablonului, de exemplu, schimba conținutul variabilei etc. Încercați:
Nu este grozav! Latte face escaparea contextuală sensibilă automat, astfel încât programatorul:
- nu trebuie să se gândească sau să știe cum se escapează unde
- nu poate greși
- nu poate uita de escapare
Acestea nu sunt nici măcar toate contextele pe care Latte le distinge la afișare și pentru care adaptează tratarea datelor. Vom parcurge acum alte cazuri interesante.
Cum să hackuiți sistemele naive
Pe câteva exemple practice, vom arăta cât de importantă este distingerea contextelor și de ce sistemele de șabloane naive nu oferă o protecție suficientă împotriva XSS, spre deosebire de Latte. Ca reprezentant al unui sistem naiv, vom folosi Twig în exemple, dar același lucru este valabil și pentru alte sisteme.
Vulnerabilitatea prin atribut
Vom încerca să injectăm cod malițios în pagină folosind un atribut HTML, așa cum am arătat mai sus. Să avem un șablon în Twig care afișează o imagine:
<img src={{ imageFile }} alt={{ imageAlt }}>
Observați că în jurul valorilor atributelor nu există ghilimele. Coderul le-ar fi putut uita, ceea ce se întâmplă pur și simplu. De exemplu, în React, codul se scrie astfel, fără ghilimele, iar un coder care alternează limbajele poate uita ușor de ghilimele.
Atacatorul introduce ca descriere a imaginii un șir inteligent construit foo onload=alert('Hacked!')
. Știm deja
că Twig nu poate recunoaște dacă variabila se afișează în fluxul textului HTML, în interiorul unui atribut, comentariu HTML
etc., pe scurt, nu distinge contextele. Și doar convertesc mecanic caracterele < > & ' "
în entități
HTML. Deci, codul rezultat va arăta astfel:
<img src=photo0145.webp alt=foo onload=alert('Hacked!')>
Și a apărut o gaură de securitate!
Un atribut onload
falsificat a devenit parte a paginii, iar browserul îl execută imediat după descărcarea
imaginii.
Acum să vedem cum se descurcă Latte cu același șablon:
<img src={$imageFile} alt={$imageAlt}>
Latte vede șablonul la fel ca dvs. Spre deosebire de Twig, înțelege HTML și știe că variabila se afișează ca valoare a unui atribut care nu este între ghilimele. De aceea, le completează. Când atacatorul introduce aceeași descriere, codul rezultat va arăta astfel:
<img src="photo0145.webp" alt="foo onload=alert('Hacked!')">
Latte a prevenit cu succes XSS.
Afișarea variabilei în JavaScript
Datorită escapării contextuale sensibile, este posibil să utilizați variabile PHP în mod complet nativ în interiorul JavaScriptului.
<p onclick="alert({$movie})">{$movie}</p>
<script>var movie = {$movie};</script>
Dacă variabila $movie
conține șirul 'Amarcord & 8 1/2'
, se va genera următoarea ieșire.
Observați că în interiorul HTML se folosește o escapare diferită decât în interiorul JavaScriptului și încă alta în
atributul onclick
:
<p onclick="alert("Amarcord & 8 1\/2")">Amarcord & 8 1/2</p>
<script>var movie = "Amarcord & 8 1\/2";</script>
Verificarea linkurilor
Latte verifică automat dacă variabila utilizată în atributele src
sau href
conține o adresă
URL web (adică protocol HTTP) și previne afișarea linkurilor care pot reprezenta un risc de securitate.
{var $link = 'javascript:attack()'}
<a href={$link}>click</a>
Afișează:
<a href="">click</a>
Verificarea poate fi dezactivată folosind filtrul nocheck.
Limitele Latte
Latte nu este o protecție completă împotriva XSS pentru întreaga aplicație. Nu am dori să încetați să vă gândiți la securitate atunci când utilizați Latte. Scopul Latte este de a asigura că un atacator nu poate modifica structura paginii, falsifica elemente sau atribute HTML. Dar nu controlează corectitudinea conținutului datelor afișate. Sau corectitudinea comportamentului JavaScriptului. Acest lucru depășește competențele sistemului de șabloane. Verificarea corectitudinii datelor, în special a celor introduse de utilizator și, prin urmare, nesigure, este o sarcină importantă a programatorului.