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

Във всички задачи ще разгледате един и същ
въпрос: как правилно да покажете
променливата $str
в HTML страница, без да
създадете XSS
уязвимост. Основата на защитата е
escaping, което означава заместване на
символи със специално значение със
съответните последователности. Например
при извеждане на низ в HTML текст, в който
символът <
има специално значение
(указва началото на таг), го заменяме с HTML
ентитета <
, а браузърът коректно
извежда символа <
.
Бъдете нащрек, защото XSS уязвимостта е много сериозна. Тя може да доведе до това нападател да поеме контрол над страницата или дори над потребителски акаунт. Успех и нека успеете да запазите HTML страницата си в безопасност!
Първа тройка въпроси
Посочете кои знаци и по какъв начин трябва да бъдат обработени в първия, втория и третия пример:
1) <p><?= $str ?></p>
2) <input value="<?= $str ?>">
3) <input value='<?= $str ?>'>
Ако изходът не беше обработен по никакъв
начин, той щеше да стане част от показаната
страница. Ако нападател успее да вкара в
променливата низ 'foo" onclick="evilCode()'
и
изходът не бъде обработен, това би
причинило изпълнението на неговия код при
кликване върху елемента:
$str = 'foo" onclick="evilCode()'
❌ без обработка: <input value="foo" onclick="evilCode()">
✅ с обработка: <input value="foo" onclick="evilCode()">
Решения на отделните примери:
- знаците
<
и&
представляват начало на HTML таг и ентитет, заменяме ги с<
и&
- знаците
"
и&
представляват край на стойността на атрибута и начало на HTML ентитет, заменяме ги с"
и&
- знаците
'
и&
представляват край на стойността на атрибута и начало на HTML ентитет, заменяме ги с'
и&
За всеки правилен отговор получавате точка. Разбира се, и в трите случая могат да се заменят с ентитети и други знаци, това не пречи, но не е необходимо.
Въпрос № 4
Продължаваме напред. Кои знаци трябва да бъдат заменени при извеждане на променливата в този контекст?
<input value=<?= $str ?>>
Решение: Както виждате, тук липсват
кавички. Най-лесно е просто да добавите
кавички и след това да екранирате по същия
начин като в предишния въпрос. Съществува и
второ решение, а именно да замените в низа с
HTML ентитети интервала и всички знаци, които
имат специално значение вътре в тага, т.е.
>
, /
, =
и някои
други.
Въпрос № 5
Сега вече става по-интересно. Кои знаци трябва да бъдат обработени в този контекст:
<script>
let foo = '<?= $str ?>';
</script>
Решение: Вътре в тага <script>
правилата за екраниране се определят от
JavaScript. HTML ентитетите тук не се
използват, но важи едно специално
правило. И така, кои знаци екранираме? Вътре
в JavaScript низ екранираме, разбира се, знака
'
, който го огражда, и то с помощта на
наклонена черта, т.е. заменяме го с \'
.
Тъй като JavaScript не поддържа многоредови
низове (само като template
literal), трябва да екранираме и знаците за
край на ред. Обаче внимавайте, освен
обичайните знаци \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 framework, конкретно 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 страница, то знайте, че не споменахме далеч всички. Тогава тестът би бил много по-дълъг. Въпреки това не е нужно да сте експерт по екраниране във всеки контекст, ако вашата система за шаблони го може.
Нека тогава да ги изпробваме.
Как се справят системите за шаблони?
Всички модерни системи за шаблони се гордеят с функцията автоматично екраниране, която автоматично екранира всички изписвани променливи. Ако го правят правилно, вашият уебсайт е в безопасност. Ако го правят грешно, уебсайтът е изложен на риск от XSS уязвимост с всички сериозни последици.
Ще тестваме популярни системи за шаблони от въпросите на този тест, за да разберем колко ефективно е тяхното автоматично екраниране. Започва dTest на системите за шаблони за PHP.
Twig ❌
Първа наред е системата за шаблони Twig (версия 3.5), която
най-често се използва във връзка с framework-а
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><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '"u{2028}; </script>
❌ <p onclick="foo('"u{2028})"></p>
❌ <!-- -- --- -->
❌ <a href="javascript:evilCode()">...</a>
❌ <div id="app"> {{ foo }} </div>
Twig се провали в шест от девет теста!
За съжаление, автоматичното екраниране на 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><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '" ; </script>
❌ <p onclick="foo('" )"></p>
❌ <!-- -- --- -->
❌ <a href="javascript:evilCode()">...</a>
❌❌ <div id="app"> <?php echo e(foo); ?> </div>
Blade се провали в шест от девет теста!
Резултатът е подобен на този при Twig.
Отново важи, че автоматичното екраниране
работи само в HTML текст и атрибути и само ако
са затворени в кавички. Автоматичното
екраниране на Blade се проваля и във всички
останали примери. В контексти (5) и (6) е
необходимо да се екранира ръчно с помощта
на {{ Js::from($str) }}
. За други контексти Blade
дори не предлага функция за екраниране. Не
разполага със защита срещу изписване на
зловредна връзка (8), нито поддръжка на
шаблони за Vue (9).
Това, което обаче е изненадващо, е
провалът на директивата @php
в Blade,
което причинява изписване на собствен PHP
код директно на изхода, което виждате на
последния ред.
Smarty ❌❌❌
Сега ще тестваме най-старата система за
шаблони за PHP, която е Smarty (версия 4.3). За голяма
изненада тази система няма активно
автоматично екраниране. Трябва при
изписване на променливи или всеки път да
посочвате филтър {$var|escape}
, или да
активирате автоматичното екраниране на HTML.
Информацията за това в документацията е
доста забутана.
{$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><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '"\u2028; </script>
❌ <p onclick="foo('"\u2028)"></p>
❌ <!-- -- --- -->
❌ <a href="javascript:evilCode()">...</a>
❌ <div id="app"> {{ foo }} </div>
Smarty се провалиха в шест от девет теста!
Резултатът на пръв поглед е подобен на
този при предишните библиотеки. Smarty могат
автоматично да екранират само в HTML текст и
атрибути, и то само когато стойностите са
затворени в кавички. Навсякъде другаде се
провалят. В контексти (5) и (6) е необходимо да
се екранира ръчно с помощта на
{$str|escape:javascript}
. Само че това е
възможно само тогава, когато не е активно
автоматичното екраниране на HTML, иначе тези
екранирания се бият помежду си. Smarty са така
от гледна точка на сигурността пълен
провал на този тест.
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><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
✅ <input value="foo onclick=evilCode()">
✅ <script> let foo = "'\"\n\u2028"; </script>
✅ <p onclick="foo("'\"\n\u2028")"></p>
✅ <!-- - - - - - -->
✅ <a href="">...</a>
✅ <div id="app"> {<!-- -->{ foo }} </div>
Latte се справи отлично във всички девет задачи!
Успя да се справи с липсващите кавички при
HTML атрибутите, успя да обработи JavaScript както
в елемента <script>
, така и в
атрибутите и успя да се справи и със
забранената последователност в HTML
коментарите.
Нещо повече, предотврати ситуацията, при която кликване върху подправена връзка от нападател би могло да стартира неговия код. И успя да се справи с екранирането на тагове за Vue.
Бонус тест
Една от съществените способности на
всички системи за шаблони е работата с
блокове и свързаното с това наследяване на
шаблони. Затова ще опитаме да дадем на
всички тествани системи за шаблони още една
задача. Ще създадем блок description
,
който ще изпишем в HTML атрибут. В реалния
свят, разбира се, дефиницията на блока ще се
намира в child-шаблона, а неговото изписване в
parent-шаблона, т.е. например в layout-а. Това е
само опростена форма, но е достатъчна, за да
тестваме автоматичното екраниране при
изписване на блокове. Как се справиха?
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' 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>
За да изпратите коментар, моля, влезте в системата