Quiz: Vă puteți apăra împotriva vulnerabilității XSS?

acum un an De la David Grudl  

Testați-vă cunoștințele și abilitățile de securitate în acest test! Puteți împiedica un atacator să preia controlul unei pagini HTML?

În toate sarcinile, veți aborda aceeași întrebare: cum să afișați în mod corespunzător variabila $str într-o pagină HTML fără a crea o vulnerabilitate XSS. Baza apărării este escaping, ceea ce înseamnă înlocuirea caracterelor cu semnificații speciale cu secvențe corespunzătoare. De exemplu, la ieșirea unui șir de caractere în text HTML, în care caracterul < are o semnificație specială (indică începutul unei etichete), îl înlocuim cu entitatea HTML &lt;, iar browserul afișează corect simbolul <.

Fiți vigilenți, deoarece vulnerabilitatea XSS este foarte gravă. Ea poate determina un atacator să preia controlul unei pagini sau chiar al contului unui utilizator. Mult succes și să reușiți să păstrați pagina HTML în siguranță!

Primul trio de întrebări

Precizați ce caractere trebuie să fie tratate și cum în primul, al doilea și al treilea exemplu:

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

Dacă ieșirea nu ar fi tratată în niciun fel, ar deveni parte a paginii afișate. Dacă un atacator reușește să introducă șirul de caractere 'foo" onclick="evilCode()' într-o variabilă, iar ieșirea nu este tratată, codul său va fi executat atunci când se face clic pe element:

$str = 'foo" onclick="evilCode()'
❌ not treated: <input value="foo" onclick="evilCode()">
✅ treated:     <input value="foo&quot; onclick=&quot;evilCode()">

Soluții pentru fiecare exemplu:

  1. caracterele < și & reprezintă începutul unei etichete și entități HTML; înlocuiți-le cu &lt; și &amp;
  2. caracterele " și & reprezintă sfârșitul unei valori de atribut și începutul unei entități HTML; înlocuiți-le cu &quot; și &amp;
  3. caracterele ' și & reprezintă sfârșitul unei valori de atribut și începutul unei entități HTML; înlocuiți-le cu &apos; și &amp;

Primiți un punct pentru fiecare răspuns corect. Bineînțeles, în toate cele trei cazuri, puteți înlocui și alte caractere cu entități; acest lucru nu provoacă niciun rău, dar nu este necesar.

Întrebarea nr. 4

Mergând mai departe, ce caractere trebuie înlocuite la afișarea unei variabile în acest context?

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

Soluție: După cum puteți vedea, aici lipsesc ghilimelele. Cel mai simplu este să adăugați pur și simplu ghilimelele și apoi să scăpați ca în întrebarea anterioară. Există, de asemenea, o a doua soluție, care constă în înlocuirea spațiilor și a tuturor caracterelor care au o semnificație specială în interiorul unei etichete, cum ar fi >, /, =, și unele altele cu entități HTML.

Întrebarea nr. 5

Acum devine mai interesant. Ce personaje trebuie tratate în acest context:

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

Soluția: În interiorul <script> regulile de scăpare sunt stabilite de JavaScript. Entitățile HTML nu sunt utilizate aici, dar există o regulă specială. Deci, ce caractere scăpăm? În interiorul șirului JavaScript, scăpăm în mod natural caracterul ' care îl delimitează, folosind o backslash, înlocuindu-l cu \'. Deoarece JavaScript nu acceptă șiruri de caractere cu mai multe rânduri (cu excepția șabloanelor literale), trebuie, de asemenea, să scăpăm de caracterele newline. Cu toate acestea, rețineți că, pe lângă caracterele obișnuite \n și \r, JavaScript consideră, de asemenea, caracterele Unicode \u2028 și \u2029 ca fiind caractere newline, pe care trebuie să le scăpăm și noi. În cele din urmă, regula specială menționată: șirul nu trebuie să conțină </script. Acest lucru poate fi evitat, de exemplu, prin înlocuirea acestuia cu <\/script.

Dacă știați acest lucru, vă felicit.

Întrebarea nr. 6

Următorul context pare a fi doar o variantă a celui precedent. Credeți că tratamentul va fi diferit?

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

Soluție: Din nou, aici se aplică regulile de scăpare pentru șirurile JavaScript, dar, spre deosebire de contextul anterior, în care entitățile HTML nu au fost scăpate, aici sunt scăpate. Așadar, mai întâi, scăpăm șirul JavaScript folosind backslash-uri și apoi înlocuim caracterele speciale (" și &) cu entități HTML. Fiți atenți, ordinea corectă este importantă.

După cum puteți vedea, același literal JavaScript poate fi codificat diferit într-un fișier <script> element și în mod diferit într-un atribut!

Întrebarea nr. 7

Să ne întoarcem de la JavaScript înapoi la HTML. Ce caractere trebuie să înlocuim în interiorul comentariului și cum?

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

Soluția: "Cum să înlocuim caracterele din comentarii? În interiorul unui comentariu HTML (și XML), pot apărea toate caracterele speciale tradiționale, cum ar fi <, &, " și '. Ceea ce este interzis, și acest lucru vă poate surprinde, este perechea de caractere --. Nu este specificată evitarea acestei secvențe, așa că depinde de dumneavoastră cum să o înlocuiți. Puteți să le intercalați cu spații. Sau, de exemplu, să le înlocuiți cu ==.

Întrebarea nr. 8

Ne apropiem de final, așa că să încercăm să variem întrebarea. Încercați să vă gândiți la ceea ce trebuie să aveți grijă atunci când imprimați o variabilă în acest context:

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

Soluție: Pe lângă scăparea, este important să verificați, de asemenea, dacă URL-ul nu conține o schemă periculoasă, cum ar fi javascript:, deoarece un URL compus astfel ar executa codul atacatorului atunci când se face clic pe el.

Întrebarea nr. 9

În sfârșit, un răsfăț pentru adevărații cunoscători. Acesta este un exemplu de aplicație care utilizează un cadru JavaScript modern, mai exact Vue. Să vedem dacă vă puteți da seama la ce trebuie să fiți atenți atunci când imprimați o variabilă în interiorul elementului #app:

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

Acest cod creează o aplicație Vue care va fi redată în elementul #app. Vue interpretează conținutul acestui element ca fiind șablonul său. Iar în cadrul șablonului, acesta interpretează acoladele duble, care reprezintă ieșirea variabilelor sau apelarea codului JavaScript (de ex, {{ foo }}).

Așadar, în cadrul elementului #app, pe lângă caracterele < și &, perechea {{ are, de asemenea, o semnificație specială, pe care trebuie să o înlocuim cu o altă secvență adecvată pentru a împiedica Vue să o interpreteze ca fiind propria etichetă. Înlocuirea cu entități HTML nu ajută în acest caz. Cum să ne ocupăm de aceasta? Există un truc: inserați un comentariu HTML gol între paranteze {<!-- -->{, iar Vue ignoră această secvență.

Rezultatele testului

Cum v-ați descurcat la acest test? Câte răspunsuri corecte aveți? Dacă ați răspuns corect la cel puțin 4 întrebări, vă aflați printre primii 8% de rezolvatori – felicitări!

Cu toate acestea, pentru a asigura securitatea site-ului dvs. web este necesar să gestionați în mod corespunzător ieșirea în toate situațiile.

Dacă ați fost surprins de cât de multe contexte diferite pot apărea pe o pagină HTML tipică, să știți că nu le-am menționat nici pe departe pe toate. Acest lucru ar face ca testul să fie mult mai lung. Cu toate acestea, nu trebuie să fiți un expert în a evada în fiecare context dacă sistemul dvs. de modelare se poate descurca.

Așadar, haideți să le testăm.

Cum se comportă sistemele de modelare?

Toate sistemele moderne de creare de șabloane dispun de o funcție de autoescaping care scapă automat toate variabilele afișate. Dacă o fac corect, site-ul dumneavoastră este în siguranță. Dacă nu o fac bine, site-ul este expus riscului de vulnerabilitate XSS cu toate consecințele sale grave.

Vom testa sistemele de modelare populare din întrebările din acest test pentru a determina eficiența auto-escapării lor. Să înceapă examinarea sistemelor de modelare PHP.

Twig ❌

Primul este sistemul de modelare Twig (versiunea 3.5), cel mai des utilizat împreună cu cadrul Symfony. Îl vom însărcina să răspundă la toate întrebările testului. Variabila $str va fi întotdeauna completată cu un șir de caractere înșelător și vom vedea cum își gestionează rezultatul. Puteți vedea rezultatele în dreapta. De asemenea, puteți explora răspunsurile și comportamentul său pe terenul de joacă.

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

Twig a picat la șase din nouă teste!

Din nefericire, scăparea automată a Twig funcționează numai în textul HTML și în atributele HTML, și chiar și atunci numai atunci când acestea sunt incluse între ghilimele. În momentul în care ghilimelele lipsesc, Twig nu raportează nicio eroare și creează o gaură de securitate XSS.

Acest lucru este deosebit de neplăcut, deoarece acesta este modul în care sunt scrise valorile atributelor în biblioteci populare precum React sau Svelte. Un programator care utilizează atât Twig, cât și React poate uita în mod natural de ghilimele.

Autodeschiderea automată a Twig eșuează, de asemenea, în toate celelalte exemple. În contextele (5) și (6), este necesară o scăpare manuală folosind {{ str|escape('js') }}, în timp ce pentru alte contexte, Twig nici măcar nu oferă o funcție de scăpare. De asemenea, îi lipsește protecția împotriva imprimării unui link malițios (8) sau suportul pentru șabloanele Vue (9).

Blade ❌❌

Cel de-al doilea participant este sistemul de modelare Blade (versiunea 10.9), care este strâns integrat cu Laravel și cu ecosistemul său. Din nou, îi vom testa abilitățile la întrebările testului nostru. De asemenea, puteți explora răspunsurile sale pe terenul de joacă.

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

Lama a eșuat în șase din nouă teste!

Rezultatul este similar cu Twig. Din nou, scăparea automată funcționează numai în text și atribute HTML și numai dacă acestea sunt incluse între ghilimele. De asemenea, funcția de autoescaping a lui Blade eșuează în toate celelalte exemple. În contextele (5) și (6), este nevoie de escaping manual folosind {{ Js::from($str) }}. Pentru alte contexte, Blade nici măcar nu oferă o funcție de scăpare. De asemenea, îi lipsește protecția împotriva imprimării unui link malițios (8) sau suportul pentru șabloanele Vue (9).

Cu toate acestea, ceea ce este surprinzător este eșecul directivei @php din Blade, care determină ieșirea propriului cod PHP direct la ieșire, așa cum se vede în ultima linie.

Latte ✅

Trio-ul este încheiat de sistemul de modelare Latte (versiunea 3.0). Vom testa autodeschiderea sa. De asemenea, puteți explora răspunsurile și comportamentul său pe terenul de joacă.

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

Latte a excelat în toate cele nouă sarcini!

A reușit să gestioneze ghilimelele lipsă din atributele HTML, a procesat JavaScript atât în <script> cât și în atribute, și a tratat secvența interzisă din comentariile HTML.

Mai mult, a prevenit o situație în care, dacă se dădea click pe un link malițios furnizat de un atacator, acesta putea executa codul său. Și a reușit să gestioneze scăparea etichetelor pentru Vue.

Test bonus

Una dintre capacitățile esențiale ale tuturor sistemelor de modelare este lucrul cu blocurile și moștenirea șablonului aferent. Prin urmare, vom da tuturor sistemelor de creare de șabloane testate încă o sarcină. Vom crea un bloc description, pe care îl vom imprima într-un atribut HTML. În lumea reală, definiția blocului ar fi, bineînțeles, localizată în șablonul copil, iar ieșirea sa în șablonul părinte, cum ar fi layout-ul. Aceasta este doar o formă simplificată, dar este suficientă pentru a testa autodeschiderea la ieșirea blocurilor. Cum au funcționat?

Twig: a eșuat ❌ la ieșirea blocurilor, caracterele nu sunt evadate corespunzător

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

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




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

Blade: a eșuat ❌ la ieșirea blocurilor, caracterele nu sunt scăpate corect

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

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




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

Latte: a trecut ✅ la ieșirea blocurilor, a gestionat corect caracterele problematice

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

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




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

De ce sunt atât de multe site-uri web vulnerabile?

Autodeschiderea automată în sisteme precum Twig, Blade sau Smarty funcționează prin simpla înlocuire a cinci caractere <>"'& cu entități HTML și nu face distincție de context. Prin urmare, funcționează doar în anumite situații și eșuează în toate celelalte. Autoescaparea naivă este o caracteristică periculoasă, deoarece creează un fals sentiment de securitate.

Prin urmare, nu este surprinzător faptul că, în prezent, peste 27 % dintre site-urile web prezintă vulnerabilități critice, în principal XSS (sursa: Acunetix Web Vulnerability Report). Cum să ieșiți din această situație? Folosiți un sistem de șabloane care să distingă contextele.

Latte este singurul sistem de șabloane PHP care nu percepe un șablon ca pe un simplu șir de caractere, ci înțelege HTML. Știe ce sunt etichetele, atributele etc. Distinge contextele. Și, prin urmare, evadează corect în textul HTML, în mod diferit în interiorul tag-urilor HTML, în mod diferit în JavaScript etc.

Latte reprezintă astfel singurul sistem de creare de template-uri sigur.


În plus, datorită înțelegerii HTML, oferă minunatele n:attributes, pe care utilizatorii le adoră:

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