Quiz: vă puteți apăra de vulnerabilitatea XSS?

acum 2 ani de David Grudl  

Testați-vă cunoștințele în quiz-ul de securitate! Puteți împiedica un atacator să preia controlul asupra paginii HTML?

În toate sarcinile veți rezolva aceeași întrebare: cum să afișați corect variabila $str într-o pagină HTML, astfel încât să nu apară vulnerabilitatea XSS. Baza apărării este escaparea, ceea ce înseamnă înlocuirea caracterelor cu semnificație specială cu secvențele corespunzătoare. De exemplu, la afișarea unui șir în text HTML, în care caracterul < are o semnificație specială (semnalează începutul unui tag), îl înlocuim cu entitatea HTML &lt;, iar browserul va afișa corect simbolul <.

Fiți atenți, deoarece vulnerabilitatea XSS este foarte gravă. Poate determina un atacator să preia controlul asupra paginii sau chiar a contului de utilizator. Mult succes și să reușiți să mențineți pagina HTML în siguranță!

Prima serie de trei întrebări

Indicați ce caractere și în ce mod trebuie tratate î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 ar introduce în variabilă șirul 'foo" onclick="evilCode()' și ieșirea nu ar fi tratată, ar determina ca la clic pe element să se execute codul său:

$str = 'foo" onclick="evilCode()'
❌ fără tratare: <input value="foo" onclick="evilCode()">
✅ cu tratare:  <input value="foo&quot; onclick=&quot;evilCode()">

Soluțiile exemplelor individuale:

  1. caracterele < și & reprezintă începutul unui tag HTML și al entităților, le înlocuim cu &lt; și &amp;
  2. caracterele " și & reprezintă sfârșitul valorii atributului și începutul unei entități HTML, le înlocuim cu &quot; și &amp;
  3. caracterele ' și & reprezintă sfârșitul valorii atributului și începutul unei entități HTML, le înlocuim cu &apos; și &amp;

Pentru fiecare răspuns corect primiți un punct. Desigur, în toate cele trei cazuri se pot înlocui și alte caractere cu entități, nu deranjează cu nimic, dar nu este necesar.

Întrebarea nr. 4

Continuăm. Ce caractere trebuie înlocuite la afișarea variabilei în acest context?

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

Soluție: După cum vedeți, aici lipsesc ghilimelele. Cel mai simplu este pur și simplu să adăugați ghilimelele și apoi să escapați la fel ca în întrebarea precedentă. Există și o a doua soluție, și anume să înlocuiți în șir cu entități HTML spațiul și toate caracterele care au semnificație specială în interiorul tag-ului, adică >, /, = și altele.

Întrebarea nr. 5

Acum începe să devină mai interesant. Ce caractere trebuie tratate în acest context:

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

Soluție: În interiorul tag-ului <script>, regulile de escapare sunt determinate de JavaScript. Entitățile HTML nu se utilizează aici, însă este valabilă o regulă specială. Deci, ce caractere escapăm? În interiorul unui șir JavaScript, escapăm, desigur, caracterul ', care îl delimitează, și anume folosind bara oblică inversă, deci îl înlocuim cu \'. Deoarece JavaScript nu suportă șiruri pe mai multe rânduri (doar ca template literal), trebuie să escapăm și caracterele de sfârșit de rând. Atenție însă, pe lângă caracterele obișnuite \n și \r, JavaScript consideră ca sfârșituri de rând și caracterele unicode \u2028 și \u2029, pe care trebuie să le escapăm de asemenea. Și, în final, regula specială menționată: în șir nu trebuie să apară </script. Acest lucru se poate preveni, de exemplu, prin înlocuirea cu <\/script.

Dacă știați asta, felicitări.

Întrebarea nr. 6

Următorul context pare doar o variație a celui precedent. Ce credeți, tratarea va fi diferită?

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

Soluție: Din nou, aici se aplică regulile de escapare în șirurile JavaScript, dar spre deosebire de contextul anterior, unde nu se escapa folosind entități HTML, aici, dimpotrivă, se escapează. Deci, mai întâi escapăm șirul JavaScript folosind bare oblice inverse și apoi înlocuim caracterele cu semnificație specială (" și &) cu entități HTML. Atenție, ordinea corectă este importantă.

După cum vedeți, același literal JavaScript poate fi codificat diferit în elementul <script> și diferit într-un atribut!

Întrebarea nr. 7

Ne întoarcem de la JavaScript înapoi la HTML. Ce caractere trebuie să înlocuim în interiorul comentariului și în ce mod?

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

Soluție: în interiorul comentariului HTML (și XML), toate caracterele speciale tradiționale, cum ar fi <, &, " și ', pot apărea. Interzisă este, și asta probabil vă va surprinde, perechea de caractere --. Escaparea acestei secvențe nu este specificată, așa că depinde de dvs. cum o înlocuiți. Puteți intercala spații între ele. Sau, de exemplu, să le înlocuiți cu ==.

Întrebarea nr. 8

Ne apropiem de sfârșit, așa că vom încerca să modificăm întrebarea. Încercați să vă gândiți la ce trebuie să fiți atenți la afișarea variabilei în acest context:

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

Soluție: pe lângă escapare, este important să verificați și dacă URL-ul nu conține o schemă periculoasă precum javascript:, deoarece un URL astfel construit ar apela codul atacatorului la clic.

Întrebarea nr. 9

La final, o perlă pentru adevărații cunoscători. Este un exemplu de aplicație care utilizează un framework JavaScript modern, în speță Vue. Să vedem dacă vă dați seama la ce trebuie să fiți atenți la afișarea variabilei î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 se va randa în elementul #app. Vue înțelege conținutul acestui element ca șablonul său. Și în interiorul șablonului interpretează acoladele duble, care înseamnă afișarea unei variabile sau apelarea unui cod JavaScript (de ex. {{ foo }}).

Deci, în interiorul elementului #app, pe lângă caracterele < și &, mai are o semnificație specială perechea {{, pe care trebuie să o înlocuim cu o altă secvență corespunzătoare, astfel încât Vue să nu o interpreteze ca tag-ul său. Înlocuirea cu entități HTML nu ajută însă în acest caz. Cum să procedăm? Funcționează un truc: între acolade inserăm un comentariu HTML gol {<!-- -->{, iar Vue ignoră o astfel de secvență.

Rezultatele quiz-ului

Cum v-ați descurcat la quiz? Câte răspunsuri corecte aveți? Dacă ați răspuns corect la cel puțin 4 întrebări, vă numărați printre cei 8% cei mai buni rezolvatori – felicitări!

Însă, pentru a asigura securitatea site-ului dvs. web, este esențial să tratați corect ieșirea în toate situațiile.

Dacă v-a surprins câte contexte diferite pot apărea pe o pagină HTML obișnuită, să știți că nu le-am menționat nici pe departe pe toate. Ar fi fost un quiz mult mai lung. Cu toate acestea, nu trebuie să fiți expert în escapare în fiecare context, dacă sistemul dvs. de șabloane se descurcă.

Haideți deci să le testăm.

Cum se descurcă sistemele de șabloane?

Toate sistemele de șabloane moderne se laudă cu funcția de autoescapare, care escapează automat toate variabilele afișate. Dacă o fac corect, site-ul dvs. este în siguranță. Dacă o fac greșit, site-ul este expus riscului de vulnerabilitate XSS cu toate consecințele grave.

Vom testa sistemele de șabloane populare din întrebările acestui quiz, pentru a afla cât de eficientă este autoescaparea lor. Începe dTest-ul sistemelor de șabloane pentru PHP.

Twig ❌

Primul pe listă este sistemul de șabloane Twig (versiunea 3.5), care este cel mai frecvent utilizat în combinație cu framework-ul Symfony. Îi vom da sarcina de a răspunde la toate întrebările quiz-ului. Variabila $str va fi întotdeauna umplută cu un șir înșelător și vom vedea cum se descurcă cu afișarea sa. Rezultatele le vedeți în dreapta. Puteți, de asemenea, 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 eșuat în șase din nouă teste!

Din păcate, autoescaparea Twig funcționează doar în textul și atributele HTML, și asta în plus doar atunci când sunt închise între ghilimele. De îndată ce ghilimelele lipsesc, Twig nu raportează nicio eroare și creează o gaură de securitate XSS.

Acest lucru este deosebit de neplăcut, deoarece astfel se scriu valorile atributelor în biblioteci populare, cum ar fi React sau Svelte. Un programator care utilizează simultan Twig și React poate uita astfel în mod natural de ghilimele.

Autoescaparea Twig eșuează și în toate celelalte exemple. În contextele (5) și (6) este necesară escaparea manuală folosind {{ str|escape('js') }}, pentru alte contexte Twig nici măcar nu oferă o funcție de escapare. Nu dispune nici de protecție împotriva afișării unui link dăunător (8) sau de suport pentru șabloanele Vue (9).

Blade ❌❌

Al doilea participant este sistemul de șabloane Blade (versiunea 10.9), care este strâns integrat cu Laravel și ecosistemul său. Din nou, vom verifica abilitățile sale pe întrebările noastre de quiz. Puteți, de asemenea, 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"> &lcub;&lcub; foo &rcub;&rcub; </div>

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

Rezultatul este similar cu cel al Twig. Din nou, este valabil faptul că autoescaparea funcționează doar în textul și atributele HTML și doar dacă sunt închise între ghilimele. Autoescaparea Blade eșuează și în toate celelalte exemple. În contextele (5) și (6) este necesară escaparea manuală folosind {{ Js::from($str)->toHtml() }}. Pentru alte contexte, Blade nici măcar nu oferă o funcție de escapare. Nu dispune de protecție împotriva afișării unui link dăunător (8) și nici de suport pentru șabloanele Vue (9).

Notă: Rezultatul pentru testul 9 a fost corectat față de prima versiune a traducerii pentru a reflecta escaparea standard a HTML.

Smarty ❌❌❌

Acum vom testa cel mai vechi sistem de șabloane pentru PHP, care este Smarty (versiunea 4.3). Spre marea surpriză, acest sistem nu are activată autoescaparea automată. Trebuie deci, la afișarea variabilelor, fie să specificați de fiecare dată filtrul {$var|escape}, fie să activați autoescaparea HTML automată. Informația despre acest lucru este destul de ascunsă în documentație.

   {$str = "<'\"&"}
1) <p>{$str}</p>
2) <input value="{$str}">
3) <input value='{$str}'>

   {$str = "foo onclick=evilCode()"}
4) <input value={$str}>

   {$str = "'\"\n\u{2028}"}
5) <script>	let foo = {$str}; </script>
6) <p onclick="foo({$str})"></p>

   {$str = "-- ---"}
7) <!-- {$str} -->

   {$str = "javascript:evilCode()"}
8) <a href="{$str}">...</a>

   {$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;\u2028; </script>
❌ <p onclick="foo(&#039;&quot;\u2028)"></p>


❌ <!-- -- --- -->


❌ <a href="javascript:evilCode()">...</a>


❌ <div id="app"> {{ foo }} </div>

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

Rezultatul este, la prima vedere, similar cu cel al bibliotecilor anterioare. Smarty reușește să escapeze automat doar în textul și atributele HTML, și asta doar atunci când valorile sunt închise între ghilimele. Peste tot altundeva eșuează. În contextele (5) și (6) este necesară escaparea manuală folosind {$str|escape:javascript}. Însă acest lucru este posibil doar atunci când nu este activă autoescaparea HTML automată, altfel aceste escapări se ciocnesc între ele. Smarty sunt astfel, din punctul de vedere al securității, un eșec total al acestui test.

Latte ✅

Trio-ul este încheiat de sistemul de șabloane Latte (versiunea 3.0). Vom testa autoescaparea sa. Puteți, de asemenea, 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ă facă față ghilimelelor lipsă la atributele HTML, a reușit să proceseze JavaScript atât în elementul <script>, cât și în atribute și a reușit să facă față și secvenței interzise în comentariile HTML.

Mai mult, a prevenit situația în care clic pe un link falsificat de un atacator ar putea executa codul său. Și a reușit să facă față escapării tag-urilor pentru Vue.

Test bonus

Una dintre abilitățile esențiale ale tuturor sistemelor de șabloane este lucrul cu blocuri și moștenirea șabloanelor asociată. Vom încerca, prin urmare, să dăm tuturor sistemelor de șabloane testate încă o sarcină. Vom crea un bloc description, pe care îl vom afișa într-un atribut HTML. În lumea reală, desigur, definiția blocului s-ar afla într-un șablon copil și afișarea sa într-un șablon părinte, adică, de exemplu, layout-ul. Aceasta este doar o formă simplificată, dar suficientă pentru a testa autoescaparea la afișarea blocurilor. Cum s-au descurcat?

Twig: a eșuat ❌ la afișarea blocurilor nu tratează caracterele

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

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




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

Blade: a eșuat ❌ la afișarea blocurilor nu tratează caracterele

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

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




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

Latte: a reușit ✅ la afișarea blocurilor a tratat 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?

Autoescaparea în sisteme precum Twig, Blade sau Smarty funcționează astfel încât pur și simplu înlocuiește cinci caractere <>"'& cu entități HTML și nu distinge deloc contextul. De aceea funcționează doar în unele situații și eșuează în toate celelalte. Autoescaparea naivă este o funcție periculoasă, deoarece creează un fals sentiment de securitate.

Nu este deci surprinzător că în prezent peste 27% dintre site-urile web au vulnerabilități critice, în principal XSS (sursa: Acunetix Web Vulnerability Report). Cum ieșim din asta? Utilizați un sistem de șabloane care distinge contextele.

Latte este singurul sistem de șabloane în PHP care nu percepe șablonul doar ca un șir de caractere, ci înțelege HTML. Înțelege ce sunt tag-urile, atributele etc. Distinge contextele. Și de aceea escapează corect în textul HTML, altfel în interiorul unui tag HTML, altfel în interiorul JavaScript etc.

Latte reprezintă astfel singurul sistem de șabloane 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>