Квіз: чи захиститеся ви від уразливості XSS?

2 роки тому від 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()'
❌ без обробки: <input value="foo" onclick="evilCode()">
✅ з обробкою:  <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-сторінці, то знайте, що ми не згадали далеко не всі. Це був би квіз набагато довший. Проте вам не потрібно бути експертом з екранування в кожному контексті, якщо це вміє ваша система шаблонів.

Тож давайте їх випробуємо.

Як справляються системи шаблонів?

Усі сучасні системи шаблонів пишаються функцією автоекранування, яка автоматично екранує всі виведені змінні. Якщо вони роблять це правильно, ваш веб-сайт у безпеці. Якщо вони роблять це неправильно, веб-сайт піддається ризику вразливості XSS з усіма серйозними наслідками.

Протестуємо популярні системи шаблонів з питань цього квізу, щоб з'ясувати, наскільки ефективним є їхнє автоекранування. Починається dTest систем шаблонів для 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 не впорався в шести з дев'яти тестів!

На жаль, автоматичне екранування 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>

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>&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 не впоралися в шести з дев'яти тестів!

Результат на перший погляд подібний до попередніх бібліотек. 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>&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-атрибуті. У реальному світі, звичайно, визначення блоку знаходилося б у дочірньому шаблоні, а його виведення — у батьківському шаблоні, тобто, наприклад, у 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&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>

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