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

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

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

Міжсайтовий скриптинг (XSS)

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

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

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

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

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

echo '<p>Search results for <em>' . $search . '</em></p>';

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

<p>Search results for <em><script>alert("Hacked!")</script></em></p>

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

Можна заперечити, що поміщення коду в змінну справді призведе до виконання JavaScript, але тільки в браузері зловмисника. Як же він потрапляє до жертви? З цієї точки зору можна виділити кілька типів XSS. У нашому прикладі з пошуковою сторінкою ми говоримо про відбитий 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> рядок і т.д. недостатні. Основою дієвого захисту є послідовна санація всіх даних, що виводяться всередині сторінки.

Насамперед, це заміна всіх символів зі спеціальним значенням на інші збіжні послідовності, що на сленгу називається escaping (перший символ послідовності називається escape character, звідси й назва). Наприклад, у тексті HTML використовується символ < has a special meaning, which, if it is not to be interpreted as the beginning of a tag, must be replaced by a visually corresponding sequence, the so-called HTML entity &lt;. А браузер друкує символ.

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

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

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

ідеальний 3-очковий захист:

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

Контекстно-усвідомлене екранування

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

<p>#text</p>
<img src="#attribute">
<textarea>#rawtext</textarea>
<!-- #comment -->

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

Другий найпоширеніший контекст – це значення атрибута HTML. Воно відрізняється від тексту тим, що тут особливе значення має лапка " or ', яка відокремлює атрибут. Її потрібно писати як єдине ціле, щоб вона не сприймалася як кінець атрибута. З іншого боку, символ &lt; можна сміливо використовувати в атрибуті, бо він не має особливого значення; його не можна сприймати як початок тега чи коментаря. Але врахуйте, що в HTML ви можете писати значення атрибутів без лапок, і в цьому випадку ціла низка символів має особливе значення, тож це ще один окремий контекст.

Можливо, це вас здивує, але всередині символів <textarea> и <title> елементів, де використовується < character need not (but can) be escaped unless followed by /. Але це скоріше цікавість.

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

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

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

<style>#css-element</style>
<p style="#css-attribute"></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

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

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

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

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

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

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

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

░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░
░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░{{ text }}░░░░
- in text: <span>{{ text }}</span>
- in tag: <span {{ text }} ></span>
- in attribute: <span title='{{ text }}'></span>
- in unquoted attribute: <span title={{ text }}></span>
- in attribute containing URL: <a href="{{ text }}"></a>
- in attribute containing JavaScript: <img onload="{{ text }}">
- in attribute containing CSS: <span style="{{ text }}"></span>
- in JavaScriptu: <script>var = {{ text }}</script>
- in CSS: <style>body { content: {{ text }}; }</style>
- in comment: <!-- {{ text }} -->

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

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

Жива демонстрація

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

{* TRY TO EDIT THIS TEMPLATE *}
{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;)">

Латт успішно запобіг 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, веб-адресу (тобто протокол HTTP), і запобігає запису посилань, які можуть становити загрозу безпеці.

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

<a href={$link}>click here</a>

Пише:

<a href="">click here</a>

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

Межі Latte

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

версію: 3.0