Тест: Чи можете ви захиститися від XSS вразливостей?

рік тому Від David Grudl  

Перевірте свої знання та навички безпеки в цьому тесті! Чи можете ви перешкодити зловмиснику отримати контроль над HTML-сторінкою?

У всіх завданнях ви вирішуватимете одне й те саме питання: як правильно відобразити змінну $str на HTML-сторінці, не створюючи при цьому XSS-уразливості. Основою захисту є екранування, тобто заміна символів зі спеціальним значенням на відповідні послідовності. Наприклад, при виведенні в 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. У реальному світі визначення блоку, звичайно, буде знаходитися в дочірньому шаблоні, а його виведення – в батьківському шаблоні, наприклад, в макеті. Це лише спрощена форма, але її достатньо, щоб протестувати автообтікання при виведенні блоків. Як вони себе показали?

Гілка: не вдалося ❌ при виведенні блоків символи не екрануються належним чином

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

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




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

Лезо: не вдалося ❌ під час виведення блоків символи не екрановано належним чином

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

Останні публікації