Kviz: Ali se lahko zaščitite pred ranljivostjo XSS?

pred 11 meseci od David Grudl  

Preverite svoje znanje in varnostne veščine v tem kvizu! Ali lahko napadalcu preprečite, da bi prevzel nadzor nad stranjo HTML?

V vseh nalogah boste obravnavali isto vprašanje: kako pravilno prikazati spremenljivko $str na strani HTML, ne da bi ustvarili ranljivost XSS. Osnova obrambe je escaping, kar pomeni zamenjavo znakov s posebnim pomenom z ustreznimi zaporedji. Na primer, pri izpisu niza v besedilo HTML, v katerem ima znak < poseben pomen (označuje začetek oznake), ga nadomestimo z entiteto HTML &lt;, brskalnik pa pravilno prikaže znak <.

Bodite pozorni, saj je ranljivost XSS zelo resna. Zaradi nje lahko napadalec prevzame nadzor nad stranjo ali celo uporabniškim računom. Veliko sreče in naj vam uspe ohraniti stran HTML varno!

Prva trojica vprašanj

Določite, katere znake je treba obdelati in kako v prvem, drugem in tretjem primeru:

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

Če izpis ne bi bil obdelan na noben način, bi postal del prikazane strani. Če bi napadalcu uspelo vstaviti niz 'foo" onclick="evilCode()' v spremenljivko in izpis ne bi bil obdelan, bi se ob kliku na element izvršila njegova koda:

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

Rešitve za vsak primer:

  1. znaka < in & predstavljata začetek oznake HTML in entitete; zamenjajte ju z &lt; in &amp;
  2. znaka " in & predstavljata konec vrednosti atributa in začetek entitete HTML; nadomestite ju z &quot; in &amp;
  3. znaka ' in & predstavljata konec vrednosti atributa in začetek entitete HTML; nadomestite ju z &apos; in &amp;

Za vsak pravilen odgovor prejmete točko. Seveda lahko v vseh treh primerih z entitetami zamenjate tudi druge znake; to ne škodi, vendar ni nujno.

Vprašanje št. 4

Če nadaljujemo, katere znake je treba nadomestiti pri prikazu spremenljivke v tem kontekstu?

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

Rešitev: Kot lahko vidite, tu manjkajo narekovaji. Najlažji način je, da preprosto dodate narekovaje in nato pobegnete kot v prejšnjem vprašanju. Obstaja tudi druga rešitev, in sicer tako, da presledke in vse znake, ki imajo poseben pomen znotraj oznake, kot so >, /, = in nekateri drugi, nadomestite z entitetami HTML.

Vprašanje št. 5

Zdaj pa je še bolj zanimivo. Katere like je treba obravnavati v tem kontekstu:

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

Rešitev: Znotraj <script> so pravila escapiranja določena v JavaScriptu. Entitete HTML se tu ne uporabljajo, vendar obstaja eno posebno pravilo. Katerim znakom se izognemo? Znotraj niza JavaScript seveda pobegnemo znaku ', ki ga razmejuje, z uporabo povratne poševnice, ki jo nadomestimo z \'. Ker JavaScript ne podpira večvrstičnih nizov (razen kot literale za predloge), moramo pobegniti tudi znakom novih vrstic. Vendar se zavedajte, da JavaScript poleg običajnih znakov \n in \r za znak nove vrstice šteje tudi znaka Unicode \u2028 in \u2029, ki ju moramo prav tako pobegniti. Na koncu še omenjeno posebno pravilo: niz ne sme vsebovati znakov </script. To lahko preprečimo tako, da ga na primer nadomestimo z znakom <\/script.

Če ste to vedeli, vam čestitamo.

Vprašanje št. 6

Zdi se, da je naslednji kontekst le različica prejšnjega. Ali menite, da bo obravnava drugačna?

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

Rešitev: Za razliko od prejšnjega konteksta, kjer entitete HTML niso bile escapirane, so tukaj escapirane. Najprej se torej izognemo nizu JavaScript s povratnimi lomkami, nato pa posebne znake (" in &) nadomestimo z entitetami HTML. Pazite, pomemben je pravilen vrstni red.

Kot lahko vidite, je lahko isti literal JavaScript različno kodiran v <script> elementu in različno v atributu!

Vprašanje št. 7

Vrnimo se od JavaScripta nazaj k HTML. Katere znake moramo zamenjati znotraj komentarja in kako?

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

Rešitev: V komentarju HTML (in XML) se lahko pojavijo vsi tradicionalni posebni znaki, kot so <, &, " in '. Prepovedan pa je par znakov --, kar vas bo morda presenetilo. Izogibanje temu zaporedju ni določeno, zato je od vas odvisno, kako ga boste nadomestili. Lahko jih prepletete s presledki. Lahko pa ju na primer nadomestite s ==.

Vprašanje št. 8

Bližamo se koncu, zato poskusite vprašanje spremeniti. Poskusite razmisliti o tem, na kaj morate biti pozorni pri tiskanju spremenljivke v tem kontekstu:

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

Rešitev: Poleg eskapiranja je pomembno tudi preveriti, ali naslov URL ne vsebuje nevarne sheme, kot je javascript:, saj bi tako sestavljen naslov URL ob kliku izvršil napadalčevo kodo.

Vprašanje št. 9

Končno poslastica za prave poznavalce. To je primer aplikacije, ki uporablja sodobno ogrodje JavaScript, zlasti Vue. Poglejmo, ali lahko ugotovite, na kaj je treba biti pozoren pri izpisovanju spremenljivke znotraj elementa #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>

Ta koda ustvari aplikacijo Vue, ki se bo izrisala v elementu #app. Vue vsebino tega elementa interpretira kot svojo predlogo. V predlogi pa interpretira dvojne oglate oklepaje, ki predstavljajo izpis spremenljivke ali klic kode JavaScript (npr, {{ foo }}).

Tako ima v elementu #app poleg znakov < in & poseben pomen tudi par {{, ki ga moramo nadomestiti z drugim ustreznim zaporedjem, da ga Vue ne bi razlagal kot svojo oznako. Zamenjava z entitetami HTML v tem primeru ne pomaga. Kako se tega lotiti? Obstaja trik: med oklepaje vstavite prazen komentar HTML {<!-- -->{in Vue ne bo upošteval tega zaporedja.

Rezultati kviza

Kako ste se odrezali v kvizu? Koliko pravilnih odgovorov imate? Če ste pravilno odgovorili na vsaj 4 vprašanja, ste med 8 % najboljših reševalcev – čestitamo!

Zagotavljanje varnosti vašega spletnega mesta pa zahteva pravilno ravnanje z izhodnimi podatki v vseh situacijah.

Če vas je presenetilo, koliko različnih kontekstov se lahko pojavi na tipični strani HTML, vedite, da še zdaleč nismo omenili vseh. S tem bi bil kviz veliko daljši. Kljub temu vam ni treba biti strokovnjak za pobege v vseh kontekstih, če to zmore vaš sistem za oblikovanje predlog.

Zato jih preizkusimo.

Kako se obnesejo šablonski sistemi?

Vsi sodobni sistemi za oblikovanje predlog imajo funkcijo autoescaping, ki samodejno izriše vse izpisane spremenljivke. Če to storijo pravilno, je vaše spletno mesto varno. Če to počnejo slabo, je spletno mesto izpostavljeno tveganju ranljivosti XSS z vsemi resnimi posledicami.

Preizkusili bomo priljubljene sisteme za šabloniranje iz vprašanj v tem kvizu, da bi ugotovili učinkovitost njihovega samodejnega izrivanja. Naj se pregled sistemov za oblikovanje predlog PHP začne.

Twig ❌

Prvi je sistem za oblikovanje predlog Twig (različica 3.5), ki se najpogosteje uporablja v povezavi z ogrodjem Symfony. Zadolžili ga bomo, da odgovori na vsa vprašanja kviza. Spremenljivka $str bo vedno napolnjena z zapletenim nizom, mi pa si bomo ogledali, kako ravna s svojim izhodom. Rezultate si lahko ogledate na desni strani. Njegove odgovore in obnašanje lahko raziščete tudi na igrišču.

   {% 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 je bil neuspešen pri šestih od devetih testov!

Na žalost Twigovo samodejno izločanje deluje le pri besedilu HTML in atributih, pa še to le, če so zaprti v narekovaje. Takoj ko narekovajev ni, Twig ne sporoči nobene napake in ustvari varnostno luknjo XSS.

To je še posebej neprijetno, ker so tako zapisane vrednosti atributov v priljubljenih knjižnicah, kot sta React ali Svelte. Programer, ki uporablja tako Twig kot React, lahko povsem naravno pozabi na narekovaje.

Twigova funkcija samodejnega izpisa narekovajev je neuspešna tudi v vseh drugih primerih. V primerih (5) in (6) je potrebno ročno izpisovanje z uporabo {{ str|escape('js') }}, medtem ko za druge kontekste Twig sploh ne ponuja funkcije escapiranja. Prav tako nima zaščite pred izpisom zlonamerne povezave (8) ali podpore za predloge Vue (9).

Blade ❌❌

Drugi udeleženec je sistem za oblikovanje predlog Blade (različica 10.9), ki je tesno povezan z Laravelom in njegovim ekosistemom. Tudi tokrat bomo njegove zmožnosti preizkusili na naših kviznih vprašanjih. Njegove odgovore lahko raziščete tudi na igrišču.

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

Na šestih od devetih testov rezilo ni bilo uspešno!

Rezultat je podoben kot pri Twigu. Ponovno velja, da samodejno izločanje deluje le v besedilu HTML in atributih ter le, če so ti zaprti v narekovaje. Bladeovo samodejno izpisovanje je neuspešno tudi v vseh drugih primerih. V primerih (5) in (6) je potrebno ročno eskapiranje z uporabo {{ Js::from($str) }}. Za druge kontekste Blade sploh ne ponuja funkcije escapiranja. Prav tako nima zaščite pred izpisom zlonamerne povezave (8) ali podpore za predloge Vue (9).

Preseneča pa neuspešnost direktive @php v programu Blade, ki povzroči izpis lastne kode PHP neposredno na izhod, kot je razvidno iz zadnje vrstice.

Latte ✅

Trio zaključuje sistem za oblikovanje predlog Latte (različica 3.0). Preizkusili bomo njegovo samodejno izpisovanje. Njegove odgovore in obnašanje lahko raziščete tudi na igrišču.

   {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 je bil odličen pri vseh devetih nalogah!

Obvladal je manjkajoče narekovaje v atributih HTML, obdelal JavaScript tako v <script> v elementu in v atributih ter obravnaval prepovedano zaporedje v komentarjih HTML.

Poleg tega je preprečil situacijo, v kateri bi klik na zlonamerno povezavo, ki jo je zagotovil napadalec, lahko izvršil njegovo kodo. Poleg tega je za sistem Vue obvladal pobege oznak.

Dodatni test

Ena od bistvenih zmožnosti vseh sistemov za oblikovanje predlog je delo z bloki in s tem povezano dedovanjem predlog. Zato bomo vsem preizkušenim sistemom za oblikovanje predlog naložili še eno nalogo. Ustvarili bomo blok description, ki ga bomo izpisali v atribut HTML. V resničnem svetu bi se definicija bloka seveda nahajala v podrejeni predlogi, njegov izpis pa v nadrejeni predlogi, na primer v postavitvi. To je le poenostavljena oblika, vendar je dovolj za preizkus samodejnega izpisa pri izpisovanju blokov. Kako so se obnesle?

Twig: neuspešno ❌ pri izpisovanju blokov, znaki niso pravilno escapirani

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

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




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

Blade: ni uspelo ❌ pri izpisovanju blokov, znaki niso pravilno escaped

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

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




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

Latte: posredovano ✅ pri izpisovanju blokov, pravilno je obravnaval problematične znake

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

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




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

Zakaj je toliko spletnih mest ranljivih?

Samodejno izpisovanje v sistemih, kot so Twig, Blade ali Smarty, deluje tako, da preprosto nadomesti pet znakov <>"'& z entitetami HTML in ne razlikuje konteksta. Zato deluje le v nekaterih primerih, v vseh drugih pa je neuspešno. Naivno samodejno izpisovanje je nevarna funkcija, saj ustvarja lažen občutek varnosti.

Zato ni presenetljivo, da ima trenutno več kot 27 % spletnih mest kritične ranljivosti, predvsem XSS (vir: Acunetix Web Vulnerability Report). Kako se rešiti iz te situacije? Uporabite sistem za oblikovanje predlog, ki razlikuje kontekste.

Latte je edini sistem za oblikovanje predlog PHP, ki predloge ne dojema le kot niz znakov, temveč razume HTML. Ve, kaj so oznake, atributi itd. Razlikuje kontekste. Zato pravilno eskapira v besedilu HTML, drugače znotraj oznak HTML, drugače znotraj JavaScript itd.

Latte tako predstavlja edini varni sistem za oblikovanje predlog.


Poleg tega zaradi svojega razumevanja HTML ponuja čudovite n:atribute, ki jih uporabniki obožujejo:

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