Latte - синоним безопасности
****************************
<div class=perex>
Latte - единственная система шаблонов для PHP с действенной защитой от критической уязвимости Cross-site Scripting (XSS). Это заслуга контекстно-зависимого экранирования. Мы обсудим:
- принцип уязвимости XSS и почему она так опасна
- что делает Latte таким эффективным в защите от XSS
- как в Twig, Blade и подобных шаблонах легко возникают дыры в безопасности
</div>
Cross-site Scripting (XSS)
==========================
Cross-site Scripting (сокращённо XSS) - одна из самых частых и опасных уязвимостей веб-сайтов. Она позволяет злоумышленнику внедрить вредоносный скрипт (malware) в чужую страницу, который затем выполняется в браузере ничего не подозревающего пользователя.
Что может такой скрипт? Например, отправить злоумышленнику любое содержимое взломанной страницы, включая конфиденциальные данные, отображаемые после входа в систему. Он может изменить страницу или выполнить другие запросы от имени пользователя. Если бы это была веб-почта, он мог бы прочитать личные письма, изменить отображаемое содержимое или перенастроить параметры, например включить пересылку копий всех писем на адрес злоумышленника, чтобы получить доступ и к будущей переписке.
Именно поэтому XSS стабильно входит в число самых опасных уязвимостей. Если такая уязвимость появилась на сайте, её нужно устранить как можно скорее, чтобы ею не воспользовались.
Как возникает уязвимость?
-------------------------
Ошибка возникает там, где генерируется веб-страница и выводятся переменные. Представьте, что вы создаёте страницу поиска, в начале которой стоит абзац с искомым словом:
```php
echo '<p>Search results for <em>' . $search . '</em></p>';
```
Злоумышленник может ввести в поле поиска, а значит и в переменную `$search`, любую строку, в том числе HTML-код вроде `<script>alert("Hacked!")</script>`. Поскольку вывод не обеззараживается, он становится частью отображаемой страницы:
```latte
<p>Search results for <em><script>alert("Hacked!")</script></em></p>
```
Вместо того чтобы показать искомую строку, браузер выполняет JavaScript. И злоумышленник получает контроль над страницей.
Вы можете возразить, что вставка кода в переменную выполняет JavaScript, но только в браузере самого злоумышленника. Как он попадает к жертве? С этой точки зрения различают несколько типов XSS. В нашем примере с поиском речь идёт об *отражённом XSS*. Здесь жертву нужно обманом заставить перейти по ссылке, содержащей вредоносный код в параметре:
```
https://example.com/?search=<script>alert("Hacked!")</script>
```
Заставить пользователя кликнуть по ссылке требует некоторой социальной инженерии, но это не так уж сложно. Пользователи переходят по ссылкам, будь то в письмах или в соцсетях, особо не задумываясь. А то, что в адресе есть что-то подозрительное, можно скрыть с помощью сокращателя ссылок: пользователь увидит лишь `bit.ly/xxx`.
Но есть и вторая, гораздо более опасная форма атаки, известная как *хранимый XSS* или *постоянный XSS*, когда злоумышленнику удаётся сохранить вредоносный код на сервере так, что он автоматически вставляется в определённые страницы.
Пример - страницы, где пользователи пишут комментарии. Злоумышленник отправляет сообщение с кодом, и оно сохраняется на сервере. Если страницы недостаточно защищены, код затем выполнится в браузере каждого посетителя.
Может показаться, что суть атаки в том, чтобы протащить в страницу строку `<script>`. На самом деле "способов вставить JavaScript очень много":https://cheatsheetseries.owasp.org/cheatsheets/XSS_Filter_Evasion_Cheat_Sheet.html. Покажем пример с HTML-атрибутом. Представьте фотогалерею, где к изображениям можно добавлять подписи, отображаемые в атрибуте `alt`:
```php
echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';
```
Злоумышленнику достаточно вставить в подпись хитро составленную строку `" onload="alert('Hacked!')`, и если вывод не обеззараживается, получившийся код будет выглядеть так:
```latte
<img src="photo0145.webp" alt="" onload="alert('Hacked!')">
```
Подсунутый атрибут `onload` теперь становится частью страницы. Браузер выполнит содержащийся в нём код сразу после загрузки изображения. Взломано!
Как защититься от XSS?
----------------------
Любые попытки обнаружить атаки по чёрным спискам, например заблокировать строку `<script>`, недостаточны. Основа работающей защиты - **последовательное обеззараживание всех данных, выводимых на странице.**
Прежде всего речь о замене всех символов со специальным значением на соответствующие последовательности, что в обиходе называют **экранированием** (первый символ последовательности называется экранирующим, отсюда и название). Например, в HTML-тексте символ `<` имеет специальное значение; если он не должен восприниматься как начало тега, мы обязаны заменить его визуально соответствующей последовательностью, HTML-сущностью `<`. Тогда браузер отобразит знак "меньше".
**Принципиально важно различать контекст, в котором данные выводятся.** Потому что в разных контекстах строки обеззараживаются по-разному. В разных контекстах специальное значение имеют разные символы. Например, экранирование различается в HTML-тексте, в HTML-атрибутах, внутри некоторых специальных элементов и так далее. Подробно мы разберём это чуть ниже.
Обеззараживание лучше всего выполнять прямо в момент вывода строки на страницу: так гарантируется, что оно действительно произойдёт и ровно один раз. Ещё лучше, если обеззараживанием **автоматически** занимается сама система шаблонов. Ведь если оно не автоматическое, программист может о нём забыть. А один недосмотр означает, что сайт уязвим.
Впрочем, XSS касается не только вывода данных в шаблонах, но и других частей приложения, которые должны правильно обращаться с недоверенными данными. Например, JavaScript в вашем приложении должен использовать с недоверенными данными `innerText` или `textContent`, а не `innerHTML`. Особое внимание нужно уделять функциям, вычисляющим строки как JavaScript: `eval()`, а также `setTimeout()`, или использованию `setAttribute()` с атрибутами событий вроде `onload` и подобных. Однако это выходит за пределы того, чем занимаются шаблоны.
**Идеальная защита в 3 пунктах:**
1) Распознаёт контекст, в котором выводятся данные.
2) Обеззараживает данные по правилам этого контекста (то есть "учитывает контекст").
3) Делает это автоматически.
Контекстно-зависимое экранирование
==================================
Что именно понимается под словом "контекст"? Это место внутри документа со своими правилами обращения с выводимыми данными. Оно зависит от типа документа (HTML, XML, CSS, JavaScript, простой текст, ...) и может различаться в отдельных его частях. Например, в HTML-документе есть множество мест (контекстов), где действуют совершенно разные правила. Вы удивитесь, как их много. Вот первые четыре:
```latte
<p>#text</p>
<img src="#attribute">
<textarea>#rawtext</textarea>
<!-- #comment -->
```
Стандартный и базовый контекст HTML-страницы - это HTML-текст. Какие правила действуют здесь? Символы `<` и `&` имеют специальное значение, обозначая начало тега или сущности, поэтому мы обязаны экранировать их, заменяя HTML-сущностями (`<` превращается в `<`, `&` - в `&`).
Второй по частоте контекст - значение HTML-атрибута. От текста он отличается тем, что здесь специальное значение имеет кавычка `"` или `'`, ограничивающая атрибут. Её нужно записать как сущность, чтобы она не была истолкована как конец атрибута. И наоборот, символ `<` внутри атрибута можно использовать спокойно, потому что там у него нет специального значения: он не может быть истолкован как начало тега или комментария. Но осторожно: в HTML значения атрибутов можно писать и без кавычек, и тогда специальное значение приобретает целый ряд символов, что делает это ещё одним отдельным контекстом.
Вы удивитесь, но особые правила действуют внутри элементов `<textarea>` и `<title>`, где символ `<` не обязан (но может) экранироваться, если за ним не следует `/`. Впрочем, это скорее мелкая деталь.
Интереснее становится внутри HTML-комментариев. Здесь HTML-сущности для экранирования не применяются. Более того, ни одна спецификация не говорит, как в комментариях следует экранировать. Нужно лишь соблюдать несколько "любопытных правил":https://html.spec.whatwg.org/multipage/syntax.html#comments и избегать внутри них определённых сочетаний символов.
Контексты могут и накладываться друг на друга - это происходит, когда мы встраиваем в HTML код JavaScript или CSS. Сделать это можно двумя разными способами, через элемент или через атрибут:
```latte
<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-сущности:
```latte
<div title='Rock'n'Roll'></div>
```
Это было просто. Куда интереснее становится, когда контексты наслаиваются, например если строка становится частью JavaScript.
Сначала выведем её в самом JavaScript. То есть заключим в кавычки и одновременно экранируем содержащиеся в ней кавычки символом `\`:
```js
'Rock\'n\'Roll'
```
Мы можем добавить и вызов функции, чтобы код что-то делал:
```js
alert('Rock\'n\'Roll');
```
Если мы вставим этот код в HTML-документ через `<script>`, никаких дальнейших изменений не потребуется, потому что он не содержит запрещённой последовательности `</script`:
```latte
<script> alert('Rock\'n\'Roll'); </script>
```
Но если бы мы захотели вставить его в HTML-атрибут, нам всё же пришлось бы экранировать кавычки в HTML-сущности:
```latte
<div onclick='alert('Rock\'n\'Roll')'></div>
```
При этом вложенным контекстом может быть не только JS или CSS. Часто это и URL. Параметры в URL экранируются превращением символов со специальным значением в последовательности, начинающиеся с `%`. Пример:
```
https://example.org/?a=Jazz&b=Rock%27n%27Roll
```
А когда мы выводим эту строку в атрибуте, мы дополнительно применяем экранирование по правилам этого контекста и заменяем `&` на `&`:
```latte
<a href="https://example.org/?a=Jazz&b=Rock%27n%27Roll">
```
Если вы дочитали досюда, поздравляем, это было исчерпывающе. Теперь вы хорошо понимаете, что такое контексты и экранирование. И не переживайте, что это сложно. Latte делает всё это за вас автоматически.
Latte против наивных систем
===========================
Мы показали, как правильно экранировать в HTML-документе и насколько принципиально важно знать контекст, то есть место, где мы выводим данные. Иными словами, как работает контекстно-зависимое экранирование. И хотя это необходимое условие работающей защиты от XSS, **Latte - единственная система шаблонов для PHP, которая это умеет.**
Как такое возможно, если сегодня все системы заявляют об автоматическом экранировании? Автоматическое экранирование без знания контекста - это, в общем-то, заблуждение, которое **создаёт ложное чувство безопасности**.
Системы шаблонов вроде Twig, Laravel Blade и других не видят в шаблоне никакой структуры HTML. Поэтому они не видят и контекстов. По сравнению с Latte они слепы и наивны. Они обрабатывают только собственные теги, всё остальное для них - незначительный поток символов:
<div class="juxtapose juxtapose--dark-handle" data-startingposition="80" data-animation="juxtapose-wiper">
```twig .{file:Шаблон Twig таким, каким его видит сам Twig}
░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░
░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░
```
```twig .{file:Шаблон Twig таким, каким его видит дизайнер}
- in text: <span>{{ foo }}</span>
- in tag: <span {{ foo }} ></span>
- in attribute: <span title='{{ foo }}'></span>
- in unquoted attribute: <span title={{ foo }}></span>
- in attribute containing URL: <a href="{{ foo }}"></a>
- in attribute containing JavaScript: <img onload="{{ foo }}">
- in attribute containing CSS: <span style="{{ foo }}"></span>
- in JavaScript: <script>var = {{ foo }}</script>
- in CSS: <style>body { content: {{ foo }}; }</style>
- in comment: <!-- {{ foo }} -->
```
</div>
Наивные системы просто механически превращают символы `< > & ' "` в HTML-сущности, и хотя в большинстве случаев это корректный способ экранирования, его далеко не всегда достаточно. Поэтому они не способны обнаружить или предотвратить возникновение самых разных дыр в безопасности, как мы покажем ниже.
Latte видит шаблон так же, как вы. Он понимает HTML и XML, распознаёт теги, атрибуты и так далее. Благодаря этому он различает отдельные контексты и обращается с данными соответственно. Тем самым он предлагает по-настоящему действенную защиту от критической уязвимости Cross-site Scripting.
<div class="juxtapose juxtapose--dark-handle" data-startingposition="80" data-animation="juxtapose-wiper">
```latte .{file:Шаблон Latte таким, каким его видит сам Latte}
░░░░░░░░░░░<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}░-->
```
```latte .{file:Шаблон Latte таким, каким его видит дизайнер}
- in text: <span>{$foo}</span>
- in tag: <span {$foo} ></span>
- in attribute: <span title='{$foo}'></span>
- in unquoted attribute: <span title={$foo}></span>
- in attribute containing URL: <a href="{$foo}"></a>
- in attribute containing JavaScript: <img onload="{$foo}">
- in attribute containing CSS: <span style="{$foo}"></span>
- in JavaScript: <script>var = {$foo}</script>
- in CSS: <style>body { content: {$foo}; }</style>
- in comment: <!-- {$foo} -->
```
</div>
Живая демонстрация
==================
Слева вы видите шаблон на Latte, справа - сгенерированный HTML-код. Переменная `$text` выводится несколько раз, каждый раз в чуть другом контексте. И потому экранируется чуть по-разному. Вы можете сами изменить код шаблона, например поменять содержимое переменной. Попробуйте:
<div class="grid fiddle wiki-expanded">
<div class="contains-pre">
``` .{file:template.latte; min-height: 14em}[fiddle-source]
{* ПОПРОБУЙТЕ ИЗМЕНИТЬ ЭТОТ ШАБЛОН *}
{var $text = "Rock'n'Roll"}
- <span>{$text}</span>
- <span title='{$text}'></span>
- <span title={$text}></span>
- <img onload="{$text}">
- <script>var = {$text}</script>
- <!-- {$text} -->
```
</div>
<div class="contains-pre">
``` .{file:view-source:...; min-height: 14em}[fiddle-output]
- <span>Rock'n'Roll</span>
- <span title='Rock'n'Roll'></span>
- <span title="Rock'n'Roll"></span>
- <img onload=""Rock'n'Roll"">
- <script>var = "Rock'n'Roll"</script>
- <!-- Rock'n'Roll -->
```
</div>
</div>
Разве не здорово! Latte выполняет контекстно-зависимое экранирование автоматически, поэтому программисту:
- не нужно думать о том, где и как экранировать, и знать это
- невозможно ошибиться
- невозможно забыть об экранировании
И это ещё не все контексты, которые Latte различает при выводе и под которые подстраивает обращение с данными. Сейчас мы разберём другие интересные случаи.
Как взломать наивные системы
============================
На нескольких практических примерах мы покажем, насколько важно различать контексты и почему наивные системы шаблонов, в отличие от Latte, не дают достаточной защиты от XSS. В примерах в роли представителя наивной системы выступит Twig, но то же самое относится и к другим системам.
Уязвимость в атрибуте
---------------------
Попробуем внедрить в страницу вредоносный код через HTML-атрибут, как мы [показали выше |#Как возникает уязвимость?]. Пусть у нас есть шаблон на Twig, выводящий изображение:
```twig .{file:Twig}
<img src={{ imageFile }} alt={{ imageAlt }}>
```
Обратите внимание, что вокруг значений атрибутов нет кавычек. Верстальщик мог о них забыть, такое просто случается. Например, в React код пишут именно так, без кавычек, и верстальщик, переключающийся между языками, легко может забыть кавычки.
Злоумышленник вставляет в подпись изображения хитро составленную строку `foo onload=alert('Hacked!')`. Мы уже знаем, что Twig не в состоянии определить, выводится ли переменная в потоке HTML-текста, внутри атрибута, в HTML-комментарии и так далее; коротко говоря, он не различает контексты. И он просто механически превращает символы `< > & ' "` в HTML-сущности. Поэтому получившийся код будет выглядеть так:
```latte
<img src=photo0145.webp alt=foo onload=alert('Hacked!')>
```
**Дыра в безопасности возникла!**
Подделанный атрибут `onload` стал частью страницы, и браузер выполнит его сразу после загрузки изображения.
Теперь посмотрим, как с тем же шаблоном справится Latte:
```latte .{file:Latte}
<img src={$imageFile} alt={$imageAlt}>
```
Latte видит шаблон так же, как вы. В отличие от Twig, он понимает HTML и знает, что переменная выводится как значение атрибута, не заключённого в кавычки. Поэтому он их добавляет. Когда злоумышленник вставит ту же подпись, получившийся код будет выглядеть так:
```latte
<img src="photo0145.webp" alt="foo onload=alert('Hacked!')">
```
**Latte успешно предотвратил XSS.**
Вывод переменной в JavaScript
-----------------------------
Благодаря контекстно-зависимому экранированию переменные PHP можно естественно использовать внутри JavaScript.
```latte
<p onclick="alert({$movie})">{$movie}</p>
<script>var movie = {$movie};</script>
```
Если переменная `$movie` содержит строку `'Amarcord & 8 1/2'`, будет сгенерирован следующий вывод. Обратите внимание на разное экранирование внутри HTML и внутри JavaScript и на ещё одно, третье экранирование в атрибуте `onclick`:
```latte
<p onclick="alert("Amarcord & 8 1/2")">Amarcord & 8 1/2</p>
<script>var movie = "Amarcord & 8 1/2";</script>
```
Проверка ссылок
---------------
Latte автоматически проверяет, содержит ли переменная, использованная в атрибутах с URL, таких как `src`, `href`, `action`, `formaction` или `data` у элемента `<object>`, безопасный URL. Он разрешает распространённые протоколы (`http`, `https`, `ftp`, `mailto`, `tel`, `sms`), а также относительные URL и блокирует потенциально опасные, например `javascript:`.
```latte
{var $link = 'javascript:attack()'}
<a href={$link}>click here</a>
```
Выводит:
```latte
<a href="">click here</a>
```
Проверку можно отключить фильтром [nocheck |filters#nocheck].
Клиентские фреймворки
---------------------
Фреймворки вроде Vue или Angular считают часть страницы своим собственным шаблоном и истолковывают внутри неё двойные фигурные скобки `{{ ... }}` как выражение. Если злоумышленнику удастся протащить такую последовательность в выводимую переменную, фреймворк вычислит её в браузере после загрузки страницы, хотя на стороне сервера никакого XSS не произошло.
Latte это предотвращает. При выводе в HTML-текст он превращает пару `{{` в `{<!-- -->{`; пустой комментарий невидим читателю, но не даёт фреймворку распознать собственный тег. Любое другое вхождение символа `{` заменяется сущностью `{` (внутри значений атрибутов так экранируется каждая `{`).
```latte
{var $query = 'Hi {{ constructor.constructor("alert(1)")() }}'}
<h1>Search: {$query}</h1>
```
Выводит:
```
<h1>Search: Hi {<!-- -->{ constructor.constructor("alert(1)")() }}</h1>
```
Защита работает автоматически и не требует настройки.
Пределы возможностей Latte
==========================
Latte - это не полная защита от XSS для всего приложения. Нам было бы жаль, если бы вы перестали думать о безопасности, используя Latte. Цель Latte в том, чтобы злоумышленник не смог изменить структуру страницы, подделать HTML-элементы или атрибуты. Но он не проверяет содержательную правильность выводимых данных. И правильность поведения JavaScript тоже. Это выходит за пределы компетенции системы шаблонов. Проверка правильности данных, особенно введённых пользователем и потому недоверенных, - важная задача программиста.
.[tip]
Хотите проверить свои знания об экранировании в отдельных контекстах? Пройдите [викторину об уязвимости XSS |https://blog.nette.org/ru/quiz-can-you-defend-against-xss-vulnerability].
Latte – синоним безопасности
Latte – единственная система шаблонов для PHP с действенной защитой от
критической уязвимости Cross-site Scripting (XSS). Это заслуга
контекстно-зависимого экранирования. Мы обсудим:
- принцип уязвимости XSS и почему она так опасна
- что делает Latte таким эффективным в защите от XSS
- как в Twig, Blade и подобных шаблонах легко возникают дыры в
безопасности
Cross-site Scripting (XSS)
Cross-site Scripting (сокращённо XSS) – одна из самых частых и опасных
уязвимостей веб-сайтов. Она позволяет злоумышленнику внедрить
вредоносный скрипт (malware) в чужую страницу, который затем выполняется в
браузере ничего не подозревающего пользователя.
Что может такой скрипт? Например, отправить злоумышленнику любое
содержимое взломанной страницы, включая конфиденциальные данные,
отображаемые после входа в систему. Он может изменить страницу или
выполнить другие запросы от имени пользователя. Если бы это была
веб-почта, он мог бы прочитать личные письма, изменить отображаемое
содержимое или перенастроить параметры, например включить пересылку
копий всех писем на адрес злоумышленника, чтобы получить доступ и к
будущей переписке.
Именно поэтому XSS стабильно входит в число самых опасных уязвимостей.
Если такая уязвимость появилась на сайте, её нужно устранить как можно
скорее, чтобы ею не воспользовались.
Как возникает уязвимость?
Ошибка возникает там, где генерируется веб-страница и выводятся
переменные. Представьте, что вы создаёте страницу поиска, в начале
которой стоит абзац с искомым словом:
echo '<p>Search results for <em>' . $search . '</em></p>';
Злоумышленник может ввести в поле поиска, а значит и в переменную
$search, любую строку, в том числе HTML-код вроде
<script>alert("Hacked!")</script>. Поскольку вывод не обеззараживается,
он становится частью отображаемой страницы:
<p>Search results for <em><script>alert("Hacked!")</script></em></p>
Вместо того чтобы показать искомую строку, браузер выполняет JavaScript. И
злоумышленник получает контроль над страницей.
Вы можете возразить, что вставка кода в переменную выполняет JavaScript,
но только в браузере самого злоумышленника. Как он попадает к жертве? С
этой точки зрения различают несколько типов XSS. В нашем примере с
поиском речь идёт об отражённом XSS. Здесь жертву нужно обманом
заставить перейти по ссылке, содержащей вредоносный код в параметре:
https://example.com/?search=<script>alert("Hacked!")</script>
Заставить пользователя кликнуть по ссылке требует некоторой
социальной инженерии, но это не так уж сложно. Пользователи переходят
по ссылкам, будь то в письмах или в соцсетях, особо не задумываясь. А то,
что в адресе есть что-то подозрительное, можно скрыть с помощью
сокращателя ссылок: пользователь увидит лишь bit.ly/xxx.
Но есть и вторая, гораздо более опасная форма атаки, известная как
хранимый XSS или постоянный 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-сущностью <. Тогда браузер
отобразит знак “меньше”.
Принципиально важно различать контекст, в котором данные
выводятся. Потому что в разных контекстах строки обеззараживаются
по-разному. В разных контекстах специальное значение имеют разные
символы. Например, экранирование различается в HTML-тексте, в
HTML-атрибутах, внутри некоторых специальных элементов и так далее.
Подробно мы разберём это чуть ниже.
Обеззараживание лучше всего выполнять прямо в момент вывода строки
на страницу: так гарантируется, что оно действительно произойдёт и
ровно один раз. Ещё лучше, если обеззараживанием автоматически
занимается сама система шаблонов. Ведь если оно не автоматическое,
программист может о нём забыть. А один недосмотр означает, что сайт
уязвим.
Впрочем, XSS касается не только вывода данных в шаблонах, но и других
частей приложения, которые должны правильно обращаться с
недоверенными данными. Например, JavaScript в вашем приложении должен
использовать с недоверенными данными innerText или textContent, а
не innerHTML. Особое внимание нужно уделять функциям, вычисляющим
строки как JavaScript: eval(), а также setTimeout(), или использованию
setAttribute() с атрибутами событий вроде onload и подобных.
Однако это выходит за пределы того, чем занимаются шаблоны.
Идеальная защита в 3 пунктах:
- Распознаёт контекст, в котором выводятся данные.
- Обеззараживает данные по правилам этого контекста (то есть
“учитывает контекст”).
- Делает это автоматически.
Контекстно-зависимое экранирование
Что именно понимается под словом “контекст”? Это место внутри
документа со своими правилами обращения с выводимыми данными. Оно
зависит от типа документа (HTML, XML, CSS, JavaScript, простой текст, …) и может
различаться в отдельных его частях. Например, в HTML-документе есть
множество мест (контекстов), где действуют совершенно разные правила.
Вы удивитесь, как их много. Вот первые четыре:
<p>#text</p>
<img src="#attribute">
<textarea>#rawtext</textarea>
<!-- #comment -->
Стандартный и базовый контекст HTML-страницы – это HTML-текст. Какие
правила действуют здесь? Символы < и & имеют
специальное значение, обозначая начало тега или сущности, поэтому мы
обязаны экранировать их, заменяя HTML-сущностями (< превращается
в <, & – в &).
Второй по частоте контекст – значение HTML-атрибута. От текста он
отличается тем, что здесь специальное значение имеет кавычка "
или ', ограничивающая атрибут. Её нужно записать как сущность,
чтобы она не была истолкована как конец атрибута. И наоборот, символ
< внутри атрибута можно использовать спокойно, потому что там
у него нет специального значения: он не может быть истолкован как
начало тега или комментария. Но осторожно: в HTML значения атрибутов
можно писать и без кавычек, и тогда специальное значение приобретает
целый ряд символов, что делает это ещё одним отдельным контекстом.
Вы удивитесь, но особые правила действуют внутри элементов
<textarea> и <title>, где символ < не обязан (но
может) экранироваться, если за ним не следует /. Впрочем, это
скорее мелкая деталь.
Интереснее становится внутри HTML-комментариев. Здесь HTML-сущности для
экранирования не применяются. Более того, ни одна спецификация не
говорит, как в комментариях следует экранировать. Нужно лишь соблюдать
несколько любопытных правил и
избегать внутри них определённых сочетаний символов.
Контексты могут и накладываться друг на друга – это происходит,
когда мы встраиваем в HTML код JavaScript или CSS. Сделать это можно двумя
разными способами, через элемент или через атрибут:
<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'n'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('Rock\'n\'Roll')'></div>
При этом вложенным контекстом может быть не только JS или CSS. Часто это
и URL. Параметры в URL экранируются превращением символов со специальным
значением в последовательности, начинающиеся с %. Пример:
https://example.org/?a=Jazz&b=Rock%27n%27Roll
А когда мы выводим эту строку в атрибуте, мы дополнительно применяем
экранирование по правилам этого контекста и заменяем & на
&:
<a href="https://example.org/?a=Jazz&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 }}░░░░
- in text: <span>{{ foo }}</span>
- in tag: <span {{ foo }} ></span>
- in attribute: <span title='{{ foo }}'></span>
- in unquoted attribute: <span title={{ foo }}></span>
- in attribute containing URL: <a href="{{ foo }}"></a>
- in attribute containing JavaScript: <img onload="{{ foo }}">
- in attribute containing CSS: <span style="{{ foo }}"></span>
- in JavaScript: <script>var = {{ foo }}</script>
- in CSS: <style>body { content: {{ foo }}; }</style>
- in comment: <!-- {{ 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}░-->
- in text: <span>{$foo}</span>
- in tag: <span {$foo} ></span>
- in attribute: <span title='{$foo}'></span>
- in unquoted attribute: <span title={$foo}></span>
- in attribute containing URL: <a href="{$foo}"></a>
- in attribute containing JavaScript: <img onload="{$foo}">
- in attribute containing CSS: <span style="{$foo}"></span>
- in JavaScript: <script>var = {$foo}</script>
- in CSS: <style>body { content: {$foo}; }</style>
- in comment: <!-- {$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'n'Roll'></span>
- <span title="Rock'n'Roll"></span>
- <img onload=""Rock'n'Roll"">
- <script>var = "Rock'n'Roll"</script>
- <!-- Rock'n'Roll -->
Разве не здорово! Latte выполняет контекстно-зависимое экранирование
автоматически, поэтому программисту:
- не нужно думать о том, где и как экранировать, и знать это
- невозможно ошибиться
- невозможно забыть об экранировании
И это ещё не все контексты, которые Latte различает при выводе и под
которые подстраивает обращение с данными. Сейчас мы разберём другие
интересные случаи.
Как взломать наивные системы
На нескольких практических примерах мы покажем, насколько важно
различать контексты и почему наивные системы шаблонов, в отличие от
Latte, не дают достаточной защиты от XSS. В примерах в роли представителя
наивной системы выступит 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('Hacked!')>
Дыра в безопасности возникла!
Подделанный атрибут onload стал частью страницы, и браузер
выполнит его сразу после загрузки изображения.
Теперь посмотрим, как с тем же шаблоном справится Latte:
<img src={$imageFile} alt={$imageAlt}>
Latte видит шаблон так же, как вы. В отличие от Twig, он понимает HTML и знает,
что переменная выводится как значение атрибута, не заключённого в
кавычки. Поэтому он их добавляет. Когда злоумышленник вставит ту же
подпись, получившийся код будет выглядеть так:
<img src="photo0145.webp" alt="foo onload=alert('Hacked!')">
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("Amarcord & 8 1/2")">Amarcord & 8 1/2</p>
<script>var movie = "Amarcord & 8 1/2";</script>
Проверка ссылок
Latte автоматически проверяет, содержит ли переменная, использованная
в атрибутах с URL, таких как src, href, action, formaction
или data у элемента <object>, безопасный URL. Он разрешает
распространённые протоколы (http, https, ftp, mailto,
tel, sms), а также относительные URL и блокирует потенциально
опасные, например javascript:.
{var $link = 'javascript:attack()'}
<a href={$link}>click here</a>
Выводит:
<a href="">click here</a>
Проверку можно отключить фильтром nocheck.
Клиентские фреймворки
Фреймворки вроде Vue или Angular считают часть страницы своим собственным
шаблоном и истолковывают внутри неё двойные фигурные скобки
{{ ... }} как выражение. Если злоумышленнику удастся протащить
такую последовательность в выводимую переменную, фреймворк вычислит
её в браузере после загрузки страницы, хотя на стороне сервера
никакого XSS не произошло.
Latte это предотвращает. При выводе в HTML-текст он превращает пару
{{ в {<!-- -->{; пустой комментарий невидим читателю, но не
даёт фреймворку распознать собственный тег. Любое другое вхождение
символа { заменяется сущностью { (внутри значений
атрибутов так экранируется каждая {).
{var $query = 'Hi {{ constructor.constructor("alert(1)")() }}'}
<h1>Search: {$query}</h1>
Выводит:
<h1>Search: Hi {<!-- -->{ constructor.constructor("alert(1)")() }}</h1>
Защита работает автоматически и не требует настройки.
Пределы возможностей Latte
Latte – это не полная защита от XSS для всего приложения. Нам было бы
жаль, если бы вы перестали думать о безопасности, используя Latte. Цель Latte
в том, чтобы злоумышленник не смог изменить структуру страницы,
подделать HTML-элементы или атрибуты. Но он не проверяет содержательную
правильность выводимых данных. И правильность поведения JavaScript тоже.
Это выходит за пределы компетенции системы шаблонов. Проверка
правильности данных, особенно введённых пользователем и потому
недоверенных, – важная задача программиста.
Хотите проверить свои знания об экранировании в отдельных
контекстах? Пройдите викторину об уязвимости
XSS.