Kviz: se boste ubranili pred ranljivostjo XSS?

pred 2 letoma od David Grudl  

Preizkusite svoje znanje v varnostnem kvizu! Ali lahko preprečite napadalcu prevzem nadzora nad HTML stranjo?

V vseh nalogah boste reševali isto vprašanje: kako pravilno izpisati spremenljivko $str v HTML strani, da ne nastane ranljivost XSS. Osnova obrambe je ubežanje znakov, kar pomeni zamenjavo znakov s posebnim pomenom z ustreznimi zaporedji. Na primer pri izpisu niza v HTML besedilu, v katerem ima znak < poseben pomen (signalizira začetek značke), ga nadomestimo s HTML entiteto &lt; in brskalnik pravilno prikaže simbol <.

Bodite na pozoru, saj je ranljivost XSS zelo resna. Lahko povzroči, da napadalec prevzame nadzor nad stranjo ali celo uporabniškim računom. Veliko uspeha in naj vam uspe ohraniti HTML stran varno!

Prva trojica vprašanj

Navedite, katere znake in na kakšen način je treba obdelati v prvem, drugem in tretjem primeru:

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

Če izpis ne bi bil nikakor obdelan, bi postal del prikazane strani. Če bi napadalec v spremenljivko dobil niz 'foo" onclick="evilCode()' in izpis ne bi bil obdelan, bi povzročil, da se ob kliku na element sproži njegova koda:

$str = 'foo" onclick="evilCode()';
❌ brez obdelave: <input value="foo" onclick="evilCode()">
✅ z obdelavo:  <input value="foo&quot; onclick=&quot;evilCode()">

Rešitve posameznih primerov:

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

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

Vprašanje št. 4

Nadaljujemo naprej. Katere znake je treba nadomestiti pri izpisu spremenljivke v tem kontekstu?

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

Rešitev: Kot vidite, tukaj manjkajo narekovaji. Najlažje je preprosto dodati narekovaje in nato ubežati enako kot v prejšnjem vprašanju. Obstaja tudi druga rešitev, in sicer nadomestiti v nizu s HTML entitetami presledek in vse znake, ki imajo poseben pomen znotraj oznake, tj. >, /, = in nekatere druge.

Vprašanje št. 5

Zdaj pa postaja bolj zanimivo. Katere znake je treba obdelati v tem kontekstu:

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

Rešitev: Znotraj značke <script> pravila za ubežanje znakov določa JavaScript. HTML entitete se tu ne uporabljajo, vendar velja eno posebno pravilo. Torej katere znake ubežimo? Znotraj JavaScript niza ubežimo seveda znak ', ki ga omejuje, in sicer s pomočjo poševnice, torej ga nadomestimo z \'. Ker JavaScript ne podpira večvrstičnih nizov (le kot template literal), moramo ubežati tudi znake konca vrstic. Vendar pozor, poleg običajnih znakov \n in \r šteje JavaScript za konce vrstic tudi unicode znake \u2028 in \u2029, ki jih moramo prav tako ubežati. In končno omenjeno posebno pravilo: v nizu se ne sme pojaviti </script. To se da preprečiti npr. z zamenjavo z <\/script.

Če ste to vedeli, čestitamo.

Vprašanje št. 6

Naslednji kontekst izgleda le kot variacija prejšnjega. Kaj mislite, se bo obdelava razlikovala?

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

Rešitev: Spet tu veljajo pravila za ubežanje znakov v JavaScript nizih, ampak za razliko od prejšnjega konteksta, kjer se s pomočjo HTML entitet ni ubežalo, se tukaj nasprotno ubeži. Torej najprej ubežimo JavaScript niz s pomočjo poševnic in nato nadomestimo znake s posebnim pomenom (" in &) s HTML entitetami. Pozor, pravilno zaporedje je pomembno.

Kot vidite, se lahko isti JavaScript literal kodira drugače v elementu <script> in drugače v atributu!

Vprašanje št. 7

Vrnimo se iz JavaScripta nazaj v HTML. Katere znake moramo nadomestiti znotraj komentarja in na kakšen način?

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

Rešitev: znotraj HTML (in XML) komentarja se lahko pojavljajo vsi tradicionalni posebni znaki, kot so <, &, " in '. Prepovedan je, in to vas bo verjetno presenetilo, par znakov --. Ubežanje tega zaporedja ni specificirano, zato je na vas, na kakšen način ga boste nadomestili. Lahko jih preložite s presledki. Ali pa na primer nadomestite z ==.

Vprašanje št. 8

Že se bližamo koncu, zato poskusimo spremeniti vprašanje. Poskusite razmisliti, na kaj je treba paziti pri izpisu spremenljivke v tem kontekstu:

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

Rešitev: poleg ubežanja znakov je pomembno še preveriti, da URL ne vsebuje nevarne sheme kot javascript:, ker bi tako sestavljen URL po kliku poklical napadalčevo kodo.

Vprašanje št. 9

Za konec biser za prave sladokusce. Gre za primer aplikacije, ki uporablja sodobno JavaScript ogrodje, konkretno Vue. Prav zanima me, ali vam pade na pamet, na kaj paziti pri izpisu 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 Vue aplikacijo, ki se bo izrisovala v element #app. Vue vsebino tega elementa razume kot svojo predlogo. In znotraj predloge interpretira dvojne zavite oklepaje, ki pomenijo izpis spremenljivke ali klic javascript kode (npr. {{ foo }}).

Torej znotraj elementa #app imata poleg znakov < in & še poseben pomen par {{, ki ga moramo nadomestiti z drugim ustreznim zaporedjem, da ga Vue ne bi interpretiral kot svojo značko. Zamenjava s HTML entitetami pa v tem primeru ne pomaga. Kako si pomagati? Deluje trik: med oklepaja vstavimo prazen HTML komentar {<!-- -->{ in Vue takšno zaporedje ignorira.

Rezultati kviza

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

Vendar je za zagotovitev varnosti vašega spletnega mesta nujno pravilno obdelati izpis v vseh situacijah.

Če vas je presenetilo, koliko različnih kontekstov se lahko pojavi na običajni HTML strani, vedite, da nismo omenili niti približno vseh. To bi bil kviz veliko daljši. Kljub temu vam ni treba biti strokovnjak za ubežanje znakov v vsakem kontekstu, če to zmore vaš sistem predlog.

Pa jih torej preizkusimo.

Kako se obnesejo sistemi predlog?

Vsi sodobni sistemi predlog se ponašajo s funkcijo samodejnega ubežanja znakov, ki samodejno ubeži vse izpisane spremenljivke. Če to počnejo pravilno, je vaše spletno mesto varno. Če to počnejo napačno, je spletno mesto izpostavljeno tveganju ranljivosti XSS z vsemi resnimi posledicami.

Preizkusili bomo priljubljene sisteme predlog iz vprašanj tega kviza, da ugotovimo, kako učinkovito je njihovo samodejno ubežanje znakov. Začenja se dTest sistemov predlog za PHP.

Twig ❌

Prvi na vrsti je sistem predlog Twig (različica 3.5), ki se najpogosteje uporablja v povezavi z ogrodjem Symfony. Dali mu bomo nalogo odgovoriti na vsa kvizna vprašanja. Spremenljivka $str bo vedno napolnjena z zvitim nizom in pogledali bomo, kako si bo poradil z njenim izpisom. Rezultate vidite desno. Njegove odgovore in obnašanje lahko tudi raziskate 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 padel na šestih od devetih testov!

Na žalost samodejno ubežanje znakov Twiga deluje le v HTML besedilu in atributih, in to še samo takrat, ko so zaprti v narekovaje. Takoj ko narekovaji manjkajo, Twig ne javi nobene napake in ustvari varnostno luknjo XSS.

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

Samodejno ubežanje znakov Twiga odpove tudi v vseh ostalih primerih. V kontekstih (5) in (6) je treba ubežati ročno s pomočjo {{ str|escape('js') }}, za druge kontekste Twig funkcije za ubežanje niti ne ponuja. Nima niti zaščite pred izpisom škodljive povezave (8) ali podpore za predloge za Vue (9).

Blade ❌❌

Drugi udeleženec je sistem predlog Blade (različica 10.9), ki je tesno integriran z Laravelem in njegovim ekosistemom. Spet bomo preverili njegove sposobnosti na naših kviznih vprašanjih. Njegove odgovore si lahko tudi raziskate 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>

Blade je padel na šestih od devetih testov!

Rezultat je podoben kot pri Twigu. Spet velja, da samodejno ubežanje znakov deluje le v HTML besedilu in atributih in le če so zaprti v narekovaje. Samodejno ubežanje znakov Blade odpove tudi v vseh ostalih primerih. V kontekstih (5) in (6) je treba ubežati ročno s pomočjo {{ Js::from($str) }}. Za druge kontekste Blade funkcije za ubežanje niti ne ponuja. Nima zaščite pred izpisom škodljive povezave (8) niti podpore za predloge za Vue (9).

Kar pa je presenetljivo, je odpoved direktive @php v Blade, kar povzroča izpis lastne PHP kode neposredno na izhod, kar vidite v zadnji vrstici.

Smarty ❌❌❌

Zdaj bomo preizkusili najstarejši sistem predlog za PHP, ki je Smarty (različica 4.3). Na veliko presenečenje ta sistem nima aktivnega samodejnega ubežanja znakov. Morate torej pri izpisovanju spremenljivk bodisi vsakič navesti filter {$var|escape}, bodisi aktivirati samodejno ubežanje znakov HTML. Informacija o tem je v dokumentaciji precej zapadla.

   {$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 je padel na šestih od devetih testov!

Rezultat je na prvi pogled podoben kot pri prejšnjih knjižnicah. Smarty zmore samodejno ubežati znake le v HTML besedilu in atributih, in to le, ko so vrednosti zaprte v narekovaje. Povsod drugje odpove. V kontekstih (5) in (6) je treba ubežati ročno s pomočjo {$str|escape:javascript}. Vendar je to mogoče le takrat, ko ni aktivno samodejno ubežanje znakov HTML, sicer se namreč ta ubežanja med seboj tepejo. Smarty so tako z vidika varnosti popoln polom tega testa.

Latte ✅

Trojico zaključuje sistem predlog Latte (različica 3.0). Preizkusili bomo njegovo samodejno ubežanje znakov. Njegove odgovore in obnašanje si lahko prav tako raziskate 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 v vseh devetih nalogah blestel!

Uspelo se je spopasti z manjkajočimi narekovaji pri HTML atributih, uspelo je obdelati JavaScript tako v elementu <script>, kot v atributih in uspelo se je spopasti tudi s prepovedanim zaporedjem v HTML komentarjih.

Še več, preprečil je situacijo, ko bi klik na podtaknjeno povezavo s strani napadalca lahko sprožil njegovo kodo. In uspelo si je poraditi z ubežanjem značk za Vue.

Bonus test

Ena izmed bistvenih sposobnosti vseh sistemov predlog je delo z bloki in s tem povezana dednost predlog. Poskusili bomo torej dati vsem testiranim sistemom predlog še eno nalogo. Ustvarili bomo blok description, ki ga bomo izpisali v HTML atributu. V realnem svetu bi se seveda definicija bloka nahajala v otroški predlogi in njegov izpis v starševski predlogi, torej na primer layoutu. To je le poenostavljena oblika, ampak zadostuje, da preizkusimo samodejno ubežanje znakov pri izpisovanju blokov. Kako so se odrezali?

Twig: padel ❌ pri izpisovanju blokov znakov ne obdeluje

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

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




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

Blade: padel ❌ pri izpisovanju blokov znakov ne obdeluje

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

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




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

Latte: uspel ✅ pri izpisovanju blokov je korektno obdelal 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 ubežanje znakov v sistemih kot so Twig, Blade ali Smarty deluje tako, da preprosto nadomešča pet znakov <>"'& s HTML entitetami in nikakor ne razlikuje konteksta. Zato deluje le v nekaterih situacijah in v vseh ostalih odpove. Naivno samodejno ubežanje znakov je nevarna funkcija, ker 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 iz tega ven? Uporabiti sistem predlog, ki razlikuje kontekste.

Latte je edini sistem predlog v PHP, ki ne dojema predloge le kot niz znakov, ampak razume HTML. Razume, kaj so značke, atributi itd. Razlikuje kontekste. In zato pravilno ubeži v HTML besedilu, drugače znotraj HTML značke, drugače znotraj JavaScripta itd.

Latte tako predstavlja edini varen sistem 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>