Latte – синоним безопасности

Latte — единственная система шаблонов для PHP с эффективной защитой от критической уязвимости межсайтового скриптинга (XSS). И это благодаря так называемому контекстно-зависимому экранированию. Мы расскажем,

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

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

Межсайтовый скриптинг (сокращенно 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. В нашем примере с поиском мы говорим об отраженном XSS. Здесь еще нужно убедить жертву кликнуть по ссылке, которая будет содержать вредоносный код в параметре:

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

Убедить пользователя перейти по ссылке хотя и требует определенной социальной инженерии, но это не так уж сложно. Пользователи кликают по ссылкам, будь то в электронных письмах или в социальных сетях, без особых раздумий. А то, что в адресе есть что-то подозрительное, можно замаскировать с помощью сокращателя URL, пользователь тогда видит только 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-сущностью &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="#атрибут">
<textarea>#rawtext</textarea>
<!-- #комментарий -->

Исходным и основным контекстом HTML-страницы является HTML-текст. Какие здесь действуют правила? Специальное значение имеют символы < и &, которые представляют начало тега или сущности, поэтому их нужно экранировать, заменив на HTML-сущность (< на &lt; & на &amp).

Вторым наиболее распространенным контекстом является значение HTML-атрибута. От текста он отличается тем, что специальное значение здесь имеет кавычка " или ', которая ограничивает атрибут. Ее нужно записать сущностью, чтобы она не воспринималась как конец атрибута. Напротив, в атрибуте можно безопасно использовать символ <, потому что здесь он не имеет специального значения, здесь он не может быть воспринят как начало тега или комментария. Но будьте осторожны, в HTML можно писать значения атрибутов и без кавычек, в таком случае специальное значение имеет целый ряд символов, то есть это еще один отдельный контекст.

Возможно, вас удивит, но специальные правила действуют внутри элементов <textarea> и <title>, где символ < не обязательно (но можно) экранировать, если за ним не следует /. Но это скорее мелочь.

Интересно то, что внутри HTML-комментариев. Здесь для экранирования не используются HTML-сущности. Даже ни одна спецификация не указывает, как следует экранировать в комментариях. Нужно лишь соблюдать несколько любопытные правила и избегать в них определенных комбинаций символов.

Контексты также могут вкладываться, что происходит, когда мы вставляем JavaScript или CSS в HTML. Это можно сделать двумя разными способами, элементом и атрибутом:

<script>#js-элемент</script>
<img onclick="#js-атрибут">

<style>#css-элемент</style>
<p style="#css-атрибут"></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, распознает теги, атрибуты и т. д. И благодаря этому различает отдельные контексты и в соответствии с ними обрабатывает данные. Таким образом, он предлагает действительно эффективную защиту от критической уязвимости межсайтового скриптинга.

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