Викторината: Можете ли да се защитите от уязвимостта 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()'
❌ not treated: <input value="foo" onclick="evilCode()">
✅ treated: <input value="foo" onclick="evilCode()">
Решения за всеки пример:
- символите
<
и&
представляват началото на HTML таг и същност; заменете ги с<
и&
- символите
"
и&
представляват края на стойност на атрибут и началото на HTML същност; заменете ги с"
и&
- символите
'
и&
представляват края на стойност на атрибут и началото на HTML същност; заменете ги с'
и&
За всеки верен отговор получавате по една точка. Разбира се, и в трите случая можете да замените и други символи със същности; това не причинява никаква вреда, но не е необходимо.
Въпрос № 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><'"&</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 работи само в 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>
Острието се провали в шест от деветте теста!
Резултатът е подобен на този при 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><'"&</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
атрибут. В реалния свят дефиницията на
блока, разбира се, ще бъде разположена в
подчинения шаблон, а изходът му – в
родителския шаблон, например в макета. Това
е само опростена форма, но тя е достатъчна,
за да се тества автоматичното изписване при
извеждане на блокове. Как се
представиха те?
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>
За да изпратите коментар, моля, влезте в системата