Latte – синонім безпеки

Latte – єдина система шаблонів для PHP з ефективним захистом від критичної вразливості Cross-site Scripting (XSS). І це завдяки так званому контекстно-залежному екрануванню. Розповімо,

  • який принцип вразливості XSS і чому вона така небезпечна
  • чому Latte настільки ефективний у захисті від XSS
  • як легко зробити дірку в безпеці в шаблонах Twig, Blade тощо

Cross-site Scripting (XSS)

Cross-site Scripting (скорочено XSS) є однією з найпоширеніших вразливостей веб-сайтів і при цьому дуже небезпечною. Вона дозволяє зловмиснику вставити на чужу сторінку шкідливий скрипт (так званий malware), який запуститься в браузері нічого не підозрюючого користувача.

Що може зробити такий скрипт? Наприклад, він може надіслати зловмиснику будь-який вміст із зараженої сторінки, включаючи конфіденційні дані, що відображаються після входу в систему. Він може змінити сторінку або виконувати інші запити від імені користувача. Якщо б це був, наприклад, веб-пошта, він міг би прочитати конфіденційні повідомлення, змінити відображуваний вміст або переналаштувати конфігурацію, наприклад, увімкнути пересилання копій усіх повідомлень на адресу зловмисника, щоб отримати доступ і до майбутніх електронних листів.

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

Як виникає вразливість?

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

echo '<p>Результати пошуку для <em>' . $search . '</em></p>';

Зловмисник може в поле пошуку і, відповідно, в змінну $search записати будь-який рядок, тобто і HTML-код, як <script>alert("Hacked!")</script>. Оскільки вивід ніяк не обробляється, він стане частиною відображеної сторінки:

<p>Результати пошуку для <em><script>alert("Hacked!")</script></em></p>

Браузер замість того, щоб вивести шуканий рядок, запустить JavaScript. І тим самим владу над сторінкою перебирає зловмисник.

Ви можете заперечити, що вставка коду в змінну хоч і призведе до запуску JavaScript, але лише в браузері зловмисника. Як він дістанеться до жертви? З цієї точки зору розрізняють кілька типів XSS. У нашому прикладі з пошуком ми говоримо про reflected XSS. Тут ще потрібно спонукати жертву клікнути на посилання, яке міститиме шкідливий код у параметрі:

https://example.com/?search=<script>alert("Hacked!")</script>

Спонукання користувача перейти за посиланням хоч і вимагає певного соціального інжинірингу, але це не надто складно. Користувачі клікають на посилання, чи то в електронних листах, чи в соціальних мережах, без особливих роздумів. А те, що в адресі є щось підозріле, можна замаскувати за допомогою скорочувача URL, користувач тоді бачить лише bit.ly/xxx.

Однак існує і друга, набагато небезпечніша форма атаки, що позначається як stored XSS або persistent XSS, коли зловмиснику вдається зберегти шкідливий код на сервері так, щоб він автоматично вставлявся в деякі сторінки.

Прикладом є сторінки, куди користувачі пишуть коментарі. Зловмисник надсилає допис, що містить код, і той зберігається на сервері. Якщо сторінки недостатньо захищені, він потім запускатиметься в браузері кожного відвідувача.

Могло б здатися, що ядро атаки полягає в тому, щоб вставити в сторінку рядок <script>. Насправді способів вставки JavaScript багато. Покажемо, наприклад, приклад вставки за допомогою HTML-атрибута. Маємо фотогалерею, де можна додавати до зображень опис, який виводиться в атрибуті alt:

echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';

Зловмиснику достатньо як опис вставити хитро складений рядок " onload="alert('Hacked!'), і якщо виведення не буде оброблено, кінцевий код виглядатиме так:

<img src="photo0145.webp" alt="" onload="alert('Hacked!')">

Частиною сторінки тепер стає підроблений атрибут onload. Браузер код, що міститься в ньому, запустить одразу після завантаження зображення. Зламано!

Як захиститися від XSS?

Будь-які спроби виявити атаку за допомогою чорного списку, наприклад, блокувати рядок <script> тощо, є недостатніми. Основою функціонального захисту є послідовна санітизація всіх даних, що виводяться всередині сторінки.

Насамперед йдеться про заміну всіх символів зі спеціальним значенням на інші відповідні послідовності, що на сленгу називається екрануванням (перший символ послідовності називається керуючим, звідси й назва). Наприклад, у тексті HTML спеціальне значення має символ <, який, якщо його не слід інтерпретувати як початок тегу, ми повинні замінити візуально відповідною послідовністю, так званою HTML-сутністю &lt;. І браузер виведе знак менше.

Дуже важливо розрізняти контекст, у якому ми виводимо дані. Оскільки в різних контекстах рядки санітизуються по-різному. У різних контекстах спеціальне значення мають різні символи. Наприклад, відрізняється екранування в тексті HTML, в атрибутах HTML, всередині деяких спеціальних елементів тощо. За мить ми розглянемо це детально.

Обробку найкраще проводити безпосередньо при виведенні рядка на сторінці, чим забезпечимо, що вона дійсно буде виконана і виконана саме один раз. Найкраще, якщо обробку забезпечить автоматично сама система шаблонів. Тому що якщо обробка не відбувається автоматично, програміст може про неї забути. А одне забуття означає, що веб-сайт вразливий.

Однак XSS стосується не лише виведення даних у шаблонах, а й інших частин програми, які повинні правильно обробляти недовірені дані. Наприклад, необхідно, щоб JavaScript у вашій програмі не використовував у зв'язку з ними innerHTML, а лише innerText або textContent. Особливу увагу слід приділяти функціям, які оцінюють рядки як JavaScript, це eval(), а також setTimeout(), або використання функції setAttribute() з подієвими атрибутами, як onload тощо. Це вже виходить за межі області, яку охоплюють шаблони.

Ідеальний захист у 3 пунктах:

  1. розпізнає контекст, у якому виводяться дані
  2. санітизує дані за правилами даного контексту (тобто «контекстно-залежно»)
  3. робить це автоматично

Контекстно-залежне екранування

Що саме мається на увазі під словом контекст? Це місце в документі з власними правилами обробки виведених даних. Воно залежить від типу документа (HTML, XML, CSS, JavaScript, plain text, …) і може відрізнятися в його конкретних частинах. Наприклад, в HTML-документі є ціла низка таких місць (контекстів), де діють дуже різні правила. Можливо, ви будете здивовані, скільки їх є. Ось перша четвірка:

<p>#text</p>
<img src="#atribut">
<textarea>#rawtext</textarea>
<!-- #komentář -->

Вихідним і базовим контекстом HTML-сторінки є HTML-текст. Які тут діють правила? Спеціальне значення мають символи < та &, які представляють початок тегу або сутності, тому їх потрібно екранувати, замінивши на HTML-сутність (< на &lt;, & на &amp).

Другим найпоширенішим контекстом є значення HTML-атрибута. Від тексту він відрізняється тим, що спеціальне значення тут має лапка " або ', яка обмежує атрибут. Її потрібно записати сутністю, щоб вона не сприймалася як кінець атрибута. Навпаки, в атрибуті можна безпечно використовувати символ <, оскільки тут він не має спеціального значення, тут він не може сприйматися як початок тегу чи коментаря. Але увага, в HTML можна писати значення атрибутів і без лапок, у такому випадку спеціальне значення має ціла низка символів, отже, це ще один окремий контекст.

Можливо, вас здивує, але спеціальні правила діють всередині елементів <textarea> та <title>, де символ < не обов'язково (але можна) екранувати, якщо за ним не слідує /. Але це скоріше дрібниця.

Цікаво всередині HTML-коментарів. Тут для екранування не використовуються HTML-сутності. Навіть жодна специфікація не вказує, як слід екранувати в коментарях. Лише необхідно дотримуватися дещо курйозних правил і уникати в них певних комбінацій символів.

Контексти також можуть нашаровуватися, що відбувається, коли ми вставляємо JavaScript або CSS в HTML. Це можна зробити двома різними способами: елементом та атрибутом:

<script>#js-element</script>
<img onclick="#js-atribut">

<style>#css-element</style>
<p style="#css-atribut"></p>

Два шляхи та два різні способи екранування даних. Всередині елемента <script> та <style>, так само як у випадку HTML-коментарів, екранування за допомогою HTML-сутностей не проводиться. При виведенні даних всередині цих елементів потрібно дотримуватися єдиного правила: текст не повинен містити послідовність </script відповідно </style.

Навпаки, в атрибутах style та on*** екранування проводиться за допомогою HTML-сутностей.

І, звичайно, всередині вкладеного JavaScript або CSS діють правила екранування цих мов. Отже, рядок в атрибуті, наприклад onload, спочатку екранується за правилами JS, а потім за правилами HTML-атрибута.

Уфф… Як бачите, HTML – це дуже складний документ, де нашаровуються контексти, і без усвідомлення того, де саме я виводжу дані (тобто в якому контексті), неможливо сказати, як це правильно зробити.

Бажаєте приклад?

Маємо рядок Rock'n'Roll.

Якщо ви будете виводити його в HTML-тексті, саме в цьому випадку не потрібно робити жодних замін, оскільки рядок не містить жодного символу зі спеціальним значенням. Інша ситуація виникне, якщо ви виведете його всередині HTML-атрибута, взятого в одинарні лапки. У такому випадку потрібно екранувати лапки на HTML-сутності:

<div title='Rock&apos;n&apos;Roll'></div>

Це було просто. Набагато цікавіша ситуація виникає при нашаруванні контекстів, наприклад, якщо рядок буде частиною JavaScript.

Спочатку виведемо його в сам JavaScript. Тобто обернемо його в лапки і водночас екрануємо за допомогою символу \ лапки, що містяться в ньому:

'Rock\'n\'Roll'

Ще можемо додати виклик якоїсь функції, щоб код щось робив:

alert('Rock\'n\'Roll');

Якщо цей код вставити в HTML-документ за допомогою <script>, не потрібно нічого додатково змінювати, оскільки в ньому не зустрічається заборонена послідовність </script:

<script> alert('Rock\'n\'Roll'); </script>

Однак, якби ми хотіли вставити його в HTML-атрибут, ми повинні ще екранувати лапки на HTML-сутності:

<div onclick='alert(&apos;Rock\&apos;n\&apos;Roll&apos;)'></div>

Але вкладеним контекстом не обов'язково має бути лише JS або CSS. Зазвичай ним є також URL. Параметри в URL екрануються так, що символи зі спеціальним значенням перетворюються на послідовності, що починаються з %. Приклад:

https://example.org/?a=Jazz&b=Rock%27n%27Roll

І коли ми виводимо цей рядок в атрибуті, ще застосовуємо екранування відповідно до цього контексту і замінюємо & на &amp:

<a href="https://example.org/?a=Jazz&amp;b=Rock%27n%27Roll">

Якщо ви дочитали до цього місця, вітаємо, це було виснажливо. Тепер ви маєте добре уявлення про те, що таке контексти та екранування. І не хвилюйтеся, що це складно. Latte це робить за вас автоматично.

Latte проти наївних систем

Ми показали, як правильно екранувати в HTML-документі та наскільки важливим є знання контексту, тобто місця, де ми виводимо дані. Іншими словами, як працює контекстно-залежне екранування. Хоча це необхідна передумова функціонального захисту від XSS, Latte – єдина система шаблонів для PHP, яка це вміє.

Як це можливо, коли всі системи сьогодні стверджують, що мають автоматичне екранування? Автоматичне екранування без знання контексту – це трохи фігня, яка створює хибне відчуття безпеки.

Системи шаблонів, такі як Twig, Laravel Blade та інші, не бачать у шаблоні жодної HTML-структури. Отже, вони не бачать і контекстів. На відміну від Latte, вони сліпі та наївні. Вони обробляють лише власні теги, все інше для них – несуттєвий потік символів:

░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░
░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░
- у тексті: <span>{{ foo }}</span>
- у тезі: <span {{ foo }} ></span>
- в атрибуті: <span title='{{ foo }}'></span>
- в атрибуті без лапок: <span title={{ foo }}></span>
- в атрибуті, що містить URL: <a href="{{ foo }}"></a>
- в атрибуті, що містить JavaScript: <img onload="{{ foo }}">
- в атрибуті, що містить CSS: <span style="{{ foo }}"></span>
- у JavaScript: <script>var = {{ foo }}</script>
- у CSS: <style>body { content: {{ foo }}; }</style>
- у коментарі: <!-- {{ foo }} -->

Наївні системи лише механічно перетворюють символи < > & ' " на HTML-сутності, що хоч і є дійсним способом екранування в більшості випадків використання, але далеко не завжди. Вони не можуть виявити або запобігти виникненню різних дір у безпеці, як ми покажемо далі.

Latte бачить шаблон так само, як і ви. Розуміє HTML, XML, розпізнає теги, атрибути тощо. І завдяки цьому розрізняє окремі контексти та відповідно до них обробляє дані. Таким чином, пропонує дійсно ефективний захист від критичної вразливості Cross-site Scripting.

░░░░░░░░░░░<span>{$foo}</span>
░░░░░░░░░░<span {$foo} ></span>
░░░░░░░░░░░░░░<span title='{$foo}'></span>
░░░░░░░░░░░░░░░░░░░░░░░░░░░<span title={$foo}></span>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<a href="{$foo}"></a>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<img onload="{$foo}">
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<span style="{$foo}"></span>
░░░░░░░░░░░░░░░░░<script>░░░░░░{$foo}</script>
░░░░░░░░░<style>░░░░░░░░░░░░░░░░{$foo}░░░</style>
░░░░░░░░░░░░░░░<!--░{$foo}░-->
- у тексті: <span>{$foo}</span>
- у тезі: <span {$foo} ></span>
- в атрибуті: <span title='{$foo}'></span>
- в атрибуті без лапок: <span title={$foo}></span>
- в атрибуті, що містить URL: <a href="{$foo}"></a>
- в атрибуті, що містить JavaScript: <img onload="{$foo}">
- в атрибуті, що містить CSS: <span style="{$foo}"></span>
- у JavaScript: <script>var = {$foo}</script>
- у CSS: <style>body { content: {$foo}; }</style>
- у коментарі: <!-- {$foo} -->

Живий приклад

Зліва ви бачите шаблон у Latte, справа – згенерований HTML-код. Кілька разів тут виводиться змінна $text, і щоразу в трохи іншому контексті. А отже, і трохи по-іншому екранована. Код шаблону ви можете редагувати самі, наприклад, змінити вміст змінної тощо. Спробуйте:

{* СПРОБУЙТЕ РЕДАГУВАТИ ЦЕЙ ШАБЛОН *}
{var $text = "Rock'n'Roll"}
- <span>{$text}</span>
- <span title='{$text}'></span>
- <span title={$text}></span>
- <img onload="{$text}">
- <script>var = {$text}</script>
- <!-- {$text} -->
- <span>Rock'n'Roll</span>
- <span title='Rock&apos;n&apos;Roll'></span>
- <span title="Rock&apos;n&apos;Roll"></span>
- <img onload="&quot;Rock&apos;n&apos;Roll&quot;">
- <script>var = "Rock'n'Roll"</script>
- <!-- Rock'n'Roll -->

Хіба це не чудово! Latte робить контекстно-залежне екранування автоматично, тому програміст:

  • не повинен думати або знати, як де екранувати
  • не може помилитися
  • не може забути про екранування

Це навіть не всі контексти, які Latte розрізняє при виведенні та для яких пристосовує обробку даних. Інші цікаві випадки ми розглянемо зараз.

Як зламати наївні системи

На кількох практичних прикладах ми покажемо, наскільки важливим є розрізнення контекстів і чому наївні системи шаблонів не забезпечують достатнього захисту від XSS, на відміну від Latte. Як представника наївної системи ми використаємо в прикладах Twig, але те саме стосується й інших систем.

Уразливість через атрибут

Спробуємо впровадити в сторінку шкідливий код за допомогою HTML-атрибута, як ми показували вище. Маємо шаблон у Twig, що відображає зображення:

<img src={{ imageFile }} alt={{ imageAlt }}>

Зверніть увагу, що навколо значень атрибутів немає лапок. Кодер міг про них забути, що просто трапляється. Наприклад, у React код пишеться так, без лапок, і кодер, який змінює мови, потім може легко забути про лапки.

Зловмисник як опис зображення вставляє хитро складений рядок foo onload=alert('Hacked!'). Ми вже знаємо, що Twig не може розпізнати, чи виводиться змінна в потоці HTML-тексту, всередині атрибута, HTML-коментаря тощо, коротше кажучи, не розрізняє контексти. І лише механічно перетворює символи < > & ' " на HTML-сутності. Отже, кінцевий код виглядатиме так:

<img src=photo0145.webp alt=foo onload=alert(&#039;Hacked!&#039;)>

І виникла дірка в безпеці!

Частиною сторінки став підроблений атрибут onload, і браузер одразу після завантаження зображення його запустить.

Тепер подивимося, як із тим самим шаблоном впорається Latte:

<img src={$imageFile} alt={$imageAlt}>

Latte бачить шаблон так само, як і ви. На відміну від Twig, він розуміє HTML і знає, що змінна виводиться як значення атрибута, який не взятий у лапки. Тому він їх доповнить. Коли зловмисник вставить той самий опис, кінцевий код виглядатиме так:

<img src="photo0145.webp" alt="foo onload=alert(&apos;Hacked!&apos;)">

Latte успішно запобіг XSS.

Виведення змінної в JavaScript

Завдяки контекстно-залежному екрануванню можна абсолютно нативно використовувати PHP-змінні всередині JavaScript.

<p onclick="alert({$movie})">{$movie}</p>

<script>var movie = {$movie};</script>

Якщо змінна $movie міститиме рядок 'Amarcord & 8 1/2', буде згенеровано наступний вивід. Зверніть увагу, що всередині HTML використовується інше екранування, ніж всередині JavaScript, і ще інше в атрибуті onclick:

<p onclick="alert(&quot;Amarcord &amp; 8 1\/2&quot;)">Amarcord &amp; 8 1/2</p>

<script>var movie = "Amarcord & 8 1\/2";</script>

Перевірка посилань

Latte автоматично перевіряє, чи містить змінна, використана в атрибутах src або href, веб-URL (тобто протокол HTTP) і запобігає виведенню посилань, які можуть становити загрозу безпеці.

{var $link = 'javascript:attack()'}

<a href={$link}>клікніть</a>

Виведе:

<a href="">клікніть</a>

Перевірку можна вимкнути за допомогою фільтра nocheck.

Обмеження Latte

Latte не є повністю комплексним захистом від XSS для всієї програми. Ми б не хотіли, щоб ви, використовуючи Latte, перестали думати про безпеку. Мета Latte – забезпечити, щоб зловмисник не міг змінити структуру сторінки, підробити HTML-елементи або атрибути. Але він не контролює правильність вмісту виведених даних. Або правильність поведінки JavaScript. Це вже виходить за межі компетенції системи шаблонів. Перевірка правильності даних, особливо тих, що введені користувачем і тому є недовіреними, є важливим завданням програміста.

версія: 3.0