Викторината: Можете ли да се защитите от уязвимостта XSS?

преди 1 година От David Grudl  

Проверете знанията и уменията си за сигурност в този тест! Можете ли да попречите на нападател да поеме контрол над HTML страница?

Във всички задачи ще разгледате един и същ въпрос: как правилно да покажете променливата $str в HTML страница, без да създадете XSS уязвимост. Основата на защитата е escaping, което означава заместване на символи със специално значение със съответните последователности. Например при извеждане на низ в HTML текст, в който символът < има специално значение (указва началото на таг), го заменяме с HTML ентитета &lt;, а браузърът коректно извежда символа <.

Бъдете бдителни, тъй като уязвимостта XSS е много сериозна. Тя може да накара нападателя да поеме контрола над страницата или дори над потребителския акаунт. Успех и дано успеете да опазите HTML страницата!

Първата тройка въпроси

Посочете кои знаци трябва да се обработват и как в първия, втория и третия пример:

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

Ако изходът не бъде обработен по никакъв начин, той ще стане част от показаната страница. Ако атакуващият успее да вмъкне низ 'foo" onclick="evilCode()' в променлива и изходът не бъде обработен, това ще доведе до изпълнение на неговия код при щракване върху елемента:

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

Решения за всеки пример:

  1. символите < и & представляват началото на HTML таг и същност; заменете ги с &lt; и &amp;
  2. символите " и & представляват края на стойност на атрибут и началото на HTML същност; заменете ги с &quot; и &amp;
  3. символите ' и & представляват края на стойност на атрибут и началото на HTML същност; заменете ги с &apos; и &amp;

За всеки верен отговор получавате по една точка. Разбира се, и в трите случая можете да замените и други символи със същности; това не причинява никаква вреда, но не е необходимо.

Въпрос № 4

Продължавайки, кои знаци трябва да бъдат заменени, когато показвате променлива в този контекст?

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

Решение: Както виждате, тук липсват кавичките. Най-лесният начин е просто да добавите кавичките и след това да ги изтриете, както в предишния въпрос. Съществува и второ решение, което се състои в това да замените интервалите и всички символи, които имат специално значение вътре в тага, като например >, /, = и някои други, с HTML единици.

Въпрос № 5

Сега става по-интересно. Кои персонажи трябва да бъдат третирани в този контекст:

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

Решение: Вътре в <script> правилата за ескапиране се определят от JavaScript. Тук не се използват HTML същности, но има едно специално правило. И така, кои символи трябва да избягаме? Вътре в низа на JavaScript естествено ескейпваме символа ', който го ограничава, като използваме обратна наклонена черта, заменяйки го с \'. Тъй като JavaScript не поддържа многоредови низове (освен като шаблонни литерали), трябва да евакуираме и знаците за нови редове. Имайте предвид обаче, че освен обичайните знаци \n и \r JavaScript счита за знаци за нов ред и Unicode знаците \u2028 и \u2029, които също трябва да избягваме. И накрая, споменатото специално правило: низът не трябва да съдържа </script. Това може да се предотврати, например чрез заместването му с <\/script.

Ако сте знаели това, поздравления.

Въпрос № 6

Следващият контекст изглежда е просто разновидност на предишния. Смятате ли, че третирането ще бъде различно?

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

Решение: Отново тук се прилагат правилата за ескапиране на низове на JavaScript, но за разлика от предишния контекст, в който HTML същностите не бяха ескапирани, тук те са ескапирани. Така че първо избягваме JavaScript низа с помощта на обратни наклонени черти и след това заменяме специалните символи (" и &) с HTML същности. Внимавайте, правилният ред е важен.

Както можете да видите, един и същ JavaScript литерал може да бъде кодиран по различен начин в <script> елемент и по различен начин в атрибут!

Въпрос № 7

Нека се върнем от JavaScript обратно към HTML. Кои символи трябва да заменим вътре в коментара и как?

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

Решение: Вътре в HTML (и XML) коментар могат да се появят всички традиционни специални символи, като например <, &, " и '. Това, което е забранено, и което може да ви изненада, е двойката знаци --. Избягването на тази последователност не е указано, така че от вас зависи как да я замените. Можете да ги размесите с интервали. Или, например, да ги замените с ==.

Въпрос № 8

Наближаваме края, затова нека се опитаме да разнообразим въпроса. Опитайте се да помислите за какво трябва да внимавате, когато отпечатвате променлива в този контекст:

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

Решение: В допълнение към ескапирането е важно да се провери дали URL адресът не съдържа опасна схема като javascript:, защото така съставен URL адрес ще изпълни кода на атакуващия, когато бъде щракнат.

Въпрос № 9

Най-накрая, едно удоволствие за истинските ценители. Това е пример за приложение, използващо модерна JavaScript рамка, по-конкретно Vue. Нека видим дали ще успеете да разберете за какво трябва да внимавате, когато отпечатвате променлива вътре в елемента #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>

Този код създава приложение на Vue, което ще бъде визуализирано в елемента #app. Vue интерпретира съдържанието на този елемент като свой шаблон. А в рамките на шаблона той интерпретира двойните къдрави скоби, които представляват извеждане на променливи или извикване на JavaScript код (напр, {{ foo }}).

И така, в рамките на елемента #app освен символите < и &, двойката {{ също има специално значение, което трябва да заменим с друга подходяща последователност, за да попречим на Vue да я интерпретира като свой таг. Заместването с HTML същности не помага в този случай. Как да се справим с него? Има един трик: вмъкнете празен HTML коментар между скобите {<!-- -->{, и Vue ще игнорира тази последователност.

Резултати от теста

Как се представихте във викторината? Колко верни отговора имате? Ако сте отговорили правилно на поне 4 въпроса, значи сте сред първите 8% от решилите теста – поздравления!

За да гарантирате сигурността на вашия уебсайт, е необходимо да обработвате правилно изходните данни във всички ситуации.

Ако сте били изненадани от това колко много различни контексти могат да се появят в една типична HTML страница, знайте, че досега не сме споменали всички. Това би направило теста много по-дълъг. Въпреки това не е необходимо да сте експерт по ескапиране във всеки контекст, ако вашата система за шаблониране може да се справи с това.

И така, нека да ги тестваме.

Как се справят системите за шаблониране?

Всички съвременни системи за шаблониране разполагат с функция autoescaping, която автоматично ескапира всички изведени променливи. Ако те го правят правилно, вашият уебсайт е в безопасност. Ако го правят лошо, сайтът е изложен на риска от XSS уязвимост с всичките ѝ сериозни последствия.

Ще тестваме популярни системи за шаблониране от въпросите в този тест, за да определим ефективността на тяхното автоматично извеждане на улов. Нека започне прегледът на системите за шаблониране на PHP.

Twig ❌

На първо място е системата за шаблониране Twig (версия 3.5), която най-често се използва във връзка с рамката Symfony. Ще ѝ възложим задачата да отговори на всички въпроси от теста. Променливата $str винаги ще бъде запълнена с труден низ и ще видим как тя се справя с извеждането му. Можете да видите резултатите вдясно. Можете също така да разгледате отговорите и поведението му на площадката.

   {% 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 работи само в HTML текст и атрибути и дори тогава само когато те са затворени в кавички. Щом кавичките липсват, Twig не съобщава за грешка и създава дупка в сигурността XSS.

Това е особено неприятно, тъй като по този начин се записват стойностите на атрибутите в популярни библиотеки като React или Svelte. Програмист, който използва едновременно Twig и React, може съвсем естествено да забрави за кавичките.

Автоматичното изписване на кавичките в Twig не успява и във всички останали примери. В контексти (5) и (6) е необходимо ръчно ескапиране с помощта на {{ str|escape('js') }}, докато за други контексти Twig дори не предлага функция за ескапиране. Липсва и защита срещу отпечатване на злонамерена връзка (8) или поддръжка на шаблони Vue (9).

Blade ❌❌

Вторият участник е системата за шаблониране Blade (версия 10.9), която е тясно интегрирана с Laravel и нейната екосистема. Отново ще тестваме нейните възможности върху въпросите от нашия тест. Можете също така да проучите отговорите й на площадката.

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

Острието се провали в шест от деветте теста!

Резултатът е подобен на този при Twig. Отново, автоматичното ескапиране работи само в HTML текст и атрибути и само ако те са затворени в кавички. Автоматичното изписване на Blade се проваля и във всички останали примери. В контексти (5) и (6) е необходимо ръчно ескапиране с помощта на {{ Js::from($str) }}. За други контексти Blade дори не предлага функция за ескапиране. Липсва и защита срещу отпечатване на злонамерена връзка (8) или поддръжка на шаблони Vue (9).

Изненадващ обаче е неуспехът на директивата @php в Blade, която предизвиква извеждането на собствения PHP код директно на изхода, както се вижда в последния ред.

Latte ✅

Триото е завършено със системата за шаблониране Latte (версия 3.0). Ще тестваме нейното автоматично изписване. Можете също така да проучите отговорите и поведението ѝ на площадката.

   {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 се справи отлично с всичките девет задачи!

Той успя да се справи с липсващи кавички в атрибути на HTML, обработи JavaScript както в <script> и в атрибути, и се справи със забранената последователност в HTML коментарите.

Нещо повече, той предотврати ситуация, при която щракването върху злонамерена връзка, предоставена от нападател, би могло да изпълни неговия код. И успя да се справи с ескапирането на тагове за Vue.

Бонус тест

Една от основните възможности на всички системи за шаблониране е работата с блокове и свързаното с тях наследяване на шаблони. Затова ще дадем на всички тествани системи за шаблониране още една задача. Ще създадем блок description, който ще отпечатаме в HTML атрибут. В реалния свят дефиницията на блока, разбира се, ще бъде разположена в подчинения шаблон, а изходът му – в родителския шаблон, например в макета. Това е само опростена форма, но тя е достатъчна, за да се тества автоматичното изписване при извеждане на блокове. Как се представиха те?

Twig: неуспешно ❌ при извеждане на блокове, символите не се ескапират правилно

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

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




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

Blade: неуспешно ❌ при извеждане на блокове, символите не са правилно ескапирани

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

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




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

Latte: премина ✅ при извеждането на блокове, правилно обработва проблемните символи

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

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




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

Защо толкова много уебсайтове са уязвими?

Автоматичното изписване в системи като Twig, Blade или Smarty работи чрез просто заместване на пет символа <>"'& с HTML единици и не прави разлика между контекста. Поради това то работи само в някои ситуации и се проваля във всички останали. Наивното автоматично изписване е опасна функция, защото създава фалшиво чувство за сигурност.

Затова не е изненадващо, че понастоящем повече от 27 % от уебсайтовете имат критични уязвимости, главно XSS (източник: Acunetix Web Vulnerability Report). Как да се измъкнем от тази ситуация? Използвайте система за шаблониране, която разграничава контекстите.

Latte е единствената система за шаблониране на PHP, която не възприема шаблона само като низ от символи, а разбира HTML. Тя знае какво представляват таговете, атрибутите и т.н. Тя различава контексти. И затова правилно ескапира в HTML текст, по различен начин в HTML тагове, по различен начин в JavaScript и т.н.

По този начин Latte представлява единствената сигурна система за шаблониране.


Освен това, благодарение на разбирането си за HTML, тя предлага чудесните n:атрибути, които потребителите обичат:

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

Последни публикации