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 <
. А браузер печатает символ.
Очень важно различать контекст, в котором выводятся данные. Потому что разные контексты по-разному санируют строки. Различные символы имеют особое значение в разных контекстах. Например, экранирование в HTML-тексте, в HTML-атрибутах, внутри некоторых специальных элементов и т.д. отличается. Мы обсудим это подробно в ближайшее время.
Лучше всего выполнять экранирование непосредственно при записи строки на странице, гарантируя, что оно действительно выполняется, и выполняется только один раз. Лучше всего, если обработка выполняется автоматически непосредственно системой шаблонов. Потому что если обработка не выполняется автоматически, программист может забыть об этом. А одно упущение означает уязвимость сайта.
Однако XSS влияет не только на вывод данных в шаблонах, но и на другие
части приложения, которые должны правильно обрабатывать недоверенные
данные. Например, JavaScript в вашем приложении не должен использовать
innerHTML
в сочетании с ними, а только innerText
или textContent
.
Особое внимание следует уделить функциям, оценивающим строки, таким
как JavaScript, который является eval()
, но также и setTimeout()
, или
использованию setAttribute()
с атрибутами событий, такими как
onload
, и т.д. Но это выходит за рамки, охватываемые шаблонами.
идеальная 3-очковая защита:
- распознавание контекста, в котором выводятся данные
- санирует данные в соответствии с правилами этого контекста (т.е. „контекстно-ориентированная“)
- делает это автоматически
Контекстно-осознанное экранирование
Что именно подразумевается под словом контекст? Это место в документе со своими правилами обращения с выводимыми данными. Оно зависит от типа документа (HTML, XML, CSS, JavaScript, обычный текст, …) и может отличаться в определенных частях документа. Например, в HTML-документе существует множество таких мест (контекстов), где применяются совершенно разные правила. Вы можете удивиться их количеству. Вот первые четыре:
<p>#text</p>
<img src="#attribute">
<textarea>#rawtext</textarea>
<!-- #comment -->
Начальным и основным контекстом HTML-страницы является HTML-текст.
Каковы здесь правила? Символы специального значения <
and
&
представляют собой начало тега или сущности, поэтому их
нужно убрать, заменив на сущность HTML (<
with <
,
&
with &
).
Второй наиболее распространенный контекст – это значение атрибута
HTML. Оно отличается от текста тем, что здесь особое значение имеет
кавычка "
or '
, которая отделяет атрибут. Ее нужно писать
как единое целое, чтобы она не воспринималась как конец атрибута. С
другой стороны, символ <
можно смело использовать в
атрибуте, потому что он не имеет особого значения; его нельзя
воспринимать как начало тега или комментария. Но учтите, что в 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'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
Когда мы выводим эту строку в атрибуте, мы все равно применяем
экранирование в соответствии с этим контекстом и заменяем &
with &
:
<a href="https://example.org/?a=Jazz&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
выводится несколько раз, каждый раз в несколько
ином контексте. И поэтому экранируется немного по-разному. Вы можете
самостоятельно отредактировать код шаблона, например, изменить
содержимое переменной и т.д. Попробуйте:
Разве это не здорово! 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('Hacked!')>
Создана брешь в системе безопасности!
Поддельный атрибут onload
стал частью страницы, и браузер
выполняет его сразу после загрузки изображения.
Теперь посмотрим, как Latte обрабатывает тот же шаблон:
<img src={$imageFile} alt={$imageAlt}>
Latte видит шаблон так же, как и вы. В отличие от Twig, он понимает HTML и знает, что переменная выводится как значение атрибута, которое не заключено в кавычки. Поэтому он добавляет их. Когда злоумышленник вставит такую же кавычку, результирующий код будет выглядеть следующим образом:
<img src="photo0145.webp" alt="foo onload=alert('Hacked!')">
Латт успешно предотвратил 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 автоматически проверяет, содержит ли переменная, используемая в
атрибутах 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. Это выходит за рамки системы шаблонов. Проверка корректности данных, особенно введенных пользователем и, следовательно, не вызывающих доверия, является важной задачей для программиста.