Викторина: Можете ли вы защититься от XSS-уязвимости?

год назад от David Grudl  

Проверьте свои знания и навыки безопасности в этой викторине! Можете ли вы предотвратить захват злоумышленником контроля над HTML-страницей?

Во всех заданиях вы будете решать один и тот же вопрос: как правильно отобразить переменную $str в HTML-странице, не создавая XSS-уязвимость. Основой защиты является escaping, то есть замена символов со специальными значениями на соответствующие последовательности. Например, при выводе строки в HTML-текст, в которой символ < имеет специальное значение (обозначает начало тега), мы заменяем его на HTML-сущность &lt;, и браузер корректно отображает символ <.

Будьте бдительны, так как XSS-уязвимость очень серьезна. Она может привести к тому, что злоумышленник получит контроль над страницей или даже учетной записью пользователя. Удачи вам и пусть вам удастся сохранить HTML-страницу в безопасности!

Первая тройка вопросов

Укажите, какие символы и как нужно обрабатывать в первом, втором и третьем примерах:

1) <p><?= $str ?></p>
2) <input value="<?= $str ?>">
3) <input value='<?= $str ?>'>

Если бы вывод никак не обрабатывался, он стал бы частью отображаемой страницы. Если бы злоумышленнику удалось вставить строку 'foo" onclick="evilCode()' в переменную, а вывод не был бы обработан, то это привело бы к выполнению его кода при нажатии на элемент:

$str = 'foo" onclick="evilCode()'
❌ not treated: <input value="foo" onclick="evilCode()">
✅ treated:     <input value="foo&quot; onclick=&quot;evilCode()">

Решения для каждого примера:

  1. символы < и & представляют собой начало HTML-тега и сущности; замените их на &lt; и &amp;
  2. символы " и & представляют конец значения атрибута и начало сущности HTML; замените их на &quot; и &amp;
  3. символы ' и & представляют конец значения атрибута и начало сущности HTML; замените их на &apos; и &amp;

За каждый правильный ответ вы получите по одному баллу. Конечно, во всех трех случаях вы можете заменить сущностями и другие символы; это не причинит вреда, но это не обязательно.

Вопрос № 4

Далее, какие символы необходимо заменить при отображении переменной в данном контексте?

<input value=<?= $str ?>>

Решение: Как вы можете видеть, здесь не хватает кавычек. Самый простой способ – просто добавить кавычки, а затем экранировать их, как в предыдущем вопросе. Есть и второе решение, которое заключается в замене пробелов и всех символов, имеющих особое значение внутри тега, таких как >, /, =, и некоторые другие, на HTML-сущности.

Вопрос № 5

Теперь становится интереснее. С какими персонажами нужно обращаться в данном контексте:

<script>
	let foo = '<?= $str ?>';
</script>

Решение: Внутри <script> правила экранирования определяются JavaScript. Здесь не используются сущности HTML, но есть одно специальное правило. Так какие символы мы должны экранировать? Внутри строки JavaScript мы, естественно, экранируем символ ', который отделяет ее, с помощью обратной косой черты, заменяя его на \'. Поскольку JavaScript не поддерживает многострочные строки (за исключением шаблонных литералов), нам также необходимо экранировать символы новой строки. Однако имейте в виду, что помимо обычных символов \n и \r, JavaScript также считает символы Юникода \u2028 и \u2029 символами новой строки, которые мы также должны экранировать. Наконец, упомянутое специальное правило: строка не должна содержать </script. Этого можно избежать, например, заменив его на <\/script.

Если вы это знали, поздравляем.

Вопрос № 6

Следующий контекст кажется просто вариацией предыдущего. Как вы думаете, будет ли лечение отличаться?

<p onclick="foo('<?= $str ?>')"></p>

Решение: Здесь снова применяются правила экранирования для строк JavaScript, но в отличие от предыдущего контекста, где сущности HTML не экранировались, здесь они экранируются. Поэтому сначала мы экранируем строку JavaScript с помощью обратных слешей, а затем заменим специальные символы (" и &) на HTML-сущности. Будьте внимательны, правильный порядок очень важен.

Как видите, один и тот же литерал JavaScript может быть по-разному закодирован в элементе <script> элементе и по-разному в атрибуте!

Вопрос № 7

Давайте вернемся от JavaScript обратно к HTML. Какие символы нам нужно заменить внутри комментария и как?

<!-- <?= $str ?> -->

Решение: Внутри HTML (и XML) комментария могут появляться все традиционные специальные символы, такие как <, &, " и '. Что запрещено, и это может вас удивить, так это пара символов --. Экранирование этой последовательности не предусмотрено, поэтому вы сами решаете, чем ее заменить. Вы можете перемежать их пробелами. Или, например, заменить их на ==.

Вопрос № 8

Мы приближаемся к концу, поэтому давайте попробуем разнообразить вопрос. Попробуйте подумать о том, что нужно быть осторожным при печати переменной в данном контексте:

<a href="<?= $str ?>">...</a>

Решение: Помимо экранирования, важно также убедиться, что URL не содержит опасной схемы типа javascript:, поскольку URL, составленный подобным образом, будет выполнять код злоумышленника при щелчке.

Вопрос № 9

Наконец, угощение для настоящих знатоков. Это пример приложения, использующего современный JavaScript-фреймворк, а именно Vue. Давайте посмотрим, сможете ли вы понять, что нужно быть осторожным при печати переменной внутри элемента #app:

<div id="app">
    <?= $str ?>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const app = new Vue({
    el: '#app',
    ...
})
</script>

Этот код создает приложение Vue, которое будет выведено в элемент #app. Vue интерпретирует содержимое этого элемента как свой шаблон. А внутри шаблона он интерпретирует двойные фигурные скобки, которые представляют собой вывод переменных или вызов кода JavaScript (например, {{ foo }}).

Таким образом, внутри элемента #app, помимо символов < и &, пара {{ также имеет особое значение, которое нам нужно заменить на другую подходящую последовательность, чтобы Vue не интерпретировал ее как свой собственный тег. Замена на HTML-сущности в данном случае не помогает. Как с этим справиться? Есть один трюк: вставьте пустой HTML-комментарий между скобками {<!-- -->{и Vue проигнорирует эту последовательность.

Результаты викторины

Как вы справились с викториной? Сколько у вас правильных ответов? Если вы правильно ответили хотя бы на 4 вопроса, вы входите в 8% лучших решателей – поздравляем!

Как бы то ни было, обеспечение безопасности вашего сайта требует правильной обработки выходных данных в любых ситуациях.

Если вы были удивлены тем, как много различных контекстов может появиться на типичной HTML-странице, знайте, что мы упомянули далеко не все из них. Это сделало бы викторину намного длиннее. Тем не менее, вам не нужно быть экспертом по экранированию в каждом контексте, если ваша система шаблонов может с этим справиться.

Итак, давайте проверим их.

Как работают системы шаблонов?

Все современные системы шаблонизации имеют функцию autoescaping, которая автоматически экранирует все выводимые переменные. Если они делают это правильно, ваш сайт в безопасности. Если они делают это плохо, сайт подвержен риску XSS-уязвимости со всеми вытекающими отсюда серьезными последствиями.

Мы протестируем популярные системы шаблонизации из вопросов этого теста, чтобы определить эффективность их автоэскейпинга. Начнем обзор систем шаблонов PHP.

Twig ❌

Первой будет система шаблонов Twig (версия 3.5), наиболее часто используемая в сочетании с фреймворком Symfony. Мы поручим ей ответить на все вопросы викторины. Переменная $str всегда будет заполнена хитрой строкой, и мы посмотрим, как она справится с выводом. Результаты вы можете увидеть справа. Вы также можете изучить его ответы и поведение на игровой площадке.

   {% set str = "<'\"&" %}
1) <p>{{ str }}</p>
2) <input value="{{ str }}">
3) <input value='{{ str }}'>

   {% set str = "foo onclick=evilCode()" %}
4) <input value={{ str }}>

   {% set str = "'\"\n\u{2028}" %}
5) <script>	let foo = '{{ str }}'; </script>
6) <p onclick="foo('{{ str }}')"></p>

   {% set str = "-- ---" %}
7) <!-- {{ str }} -->

   {% set str = "javascript:evilCode()" %}
8) <a href="{{ str }}">...</a>

   {% set str = "{{ foo }}" %}
9) <div id="app"> {{ str }} </div>

✅ <p>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script> let foo = &#039;&quot;u{2028}; </script>
❌ <p onclick="foo(&#039;&quot;u{2028})"></p>


❌ <!-- -- --- -->


❌ <a href="javascript:evilCode()">...</a>


❌ <div id="app"> {{ foo }} </div>

**Твиг провалил шесть из девяти тестов!

К сожалению, автоматическое экранирование в Twig работает только в HTML-тексте и атрибутах, да и то только тогда, когда они заключены в кавычки. Как только кавычки пропадают, Twig не сообщает об ошибке и создает XSS брешь в безопасности.

Это особенно неприятно, потому что именно так записываются значения атрибутов в популярных библиотеках, таких как React или Svelte. Программист, использующий и Twig, и React, может совершенно естественно забыть о кавычках.

Во всех остальных примерах автозавершение Twig также не работает. В контекстах (5) и (6) необходимо ручное экранирование с помощью клавиш {{ str|escape('js') }}а в других контекстах Twig даже не предлагает функции экранирования. В нем также отсутствует защита от печати вредоносной ссылки (8) и поддержка шаблонов Vue (9).

Blade ❌❌

Второй участник – это система шаблонов Blade (версия 10.9), которая тесно интегрирована с Laravel и его экосистемой. И снова мы проверим ее возможности в вопросах нашей викторины. Вы также можете изучить ее ответы на игровой площадке.

   @php($str = "<'\"&")
1) <p>{{ $str }}</p>
2) <input value="{{ $str }}">
3) <input value='{{ $str }}'>

   @php($str = "foo onclick=evilCode()")
4) <input value={{ $str }}>

   @php($str = "'\"\n\u{2028}")
5) <script> let foo = {{ $str }}; </script>
6) <p onclick="foo({{ $str }})"></p>

   @php($str = "-- ---")
7) <!-- {{ $str }} -->

   @php($str = "javascript:evilCode()")
8) <a href="{{ $str }}">...</a>

   @php($str = "{{ foo }}")
9) <div id="app"> {{ $str }} </div>

✅ <p>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script>	let foo = &#039;&quot; ; </script>
❌ <p onclick="foo(&#039;&quot; )"></p>


❌ <!-- -- --- -->


❌ <a href="javascript:evilCode()">...</a>


❌❌ <div id="app"> &lt;?php echo e(foo); ?&gt; </div>

Лезвие не прошло шесть из девяти тестов!

Результат аналогичен Twig. Опять же, автоматическое экранирование работает только в HTML-тексте и атрибутах и только если они заключены в кавычки. Во всех остальных примерах автоматическое экранирование Blade также не работает. В контекстах (5) и (6) необходимо ручное экранирование с помощью функции {{ Js::from($str) }}. Для других контекстов Blade даже не предлагает функцию экранирования. В нем также отсутствует защита от печати вредоносной ссылки (8) или поддержка шаблонов Vue (9).

Однако, что удивительно, так это неработающая директива @php в Blade, которая приводит к выводу собственного PHP-кода непосредственно в вывод, как видно из последней строки.

Латте ✅

Трио завершено системой шаблонов Latte (версия 3.0). Мы протестируем ее автоэскейпинг. Вы также можете изучить ее ответы и поведение на игровой площадке.

   {var $str = "<'\"&"}
1) <p>{$str}</p>
2) <input value="{$str}">
3) <input value='{$str}'>

   {var $str = "foo onclick=evilCode()"}
4) <input value={$str}>

   {var $str = "'\"\n\u{2028}"}
5) <script>	let foo = {$str}; </script>
6) <p onclick="foo({$str})"></p>

   {var $str = "-- ---"}
7) <!-- {$str} -->

   {var $str = "javascript:evilCode()"}
8) <a href="{$str}">...</a>

   {var $str = "{{ foo }}"}
9) <div id="app"> {$str} </div>

✅ <p>&lt;'"&amp;</p>
✅ <input value="&lt;&apos;&quot;&amp;">
✅ <input value='&lt;&apos;&quot;&amp;'>


✅ <input value="foo onclick=evilCode()">


✅ <script> let foo = "'\"\n\u2028"; </script>
✅ <p onclick="foo(&quot;&apos;\&quot;\n\u2028&quot;)"></p>


✅ <!--  - -  - - -  -->


✅ <a href="">...</a>


✅ <div id="app"> {<!-- -->{ foo }} </div>

Латте отлично справился со всеми девятью заданиями!

Он справился с отсутствующими кавычками в атрибутах HTML, обработал JavaScript как в <script> элемент и атрибуты, а также справился с запрещенной последовательностью в HTML-комментариях.

Более того, он предотвратил ситуацию, когда нажатие на вредоносную ссылку, предоставленную злоумышленником, могло привести к выполнению его кода. И она справилась с экранированием тегов для Vue.

Бонусный тест

Одной из важнейших возможностей всех систем шаблонизации является работа с блоками и связанным с ними наследованием шаблонов. Поэтому мы дадим всем протестированным шаблонизаторам еще одно задание. Мы создадим блок description, который выведем в HTML-атрибуте. В реальном мире определение блока, конечно же, будет находиться в дочернем шаблоне, а его вывод в родительском шаблоне, например, в макете. Это всего лишь упрощенная форма, но ее достаточно, чтобы проверить автозакрепление при выводе блоков. Как они себя показали?

Twig: не удалось ❌ при выводе блоков, символы не экранируются должным образом

{% block description %}
	rock n' roll
{% endblock %}

<meta name='description'
	content='{{ block('description') }}'>




<meta name='description'
	content=' rock n' roll '> ❌

Лезвие: сбой ❌ при выводе блоков, символы неправильно экранируются

@section('description')
	rock n' roll
@endsection

<meta name='description'
	content='@yield('description')'>




<meta name='description'
	content=' rock n' roll '> ❌

Латте: передал ✅ при выводе блоков, он правильно обрабатывал проблемные символы

{block description}
	rock n' roll
{/block}

<meta name='description'
	content='{include description}'>




<meta name='description'
	content=' rock n&apos; roll '> ✅

Почему так много веб-сайтов уязвимы?

Автоскейпинг в таких системах, как Twig, Blade или Smarty, работает путем простой замены пяти символов <>"'& на HTML-сущности и не различает контекст. Поэтому он работает только в некоторых ситуациях и не работает во всех остальных. Наивная автозамена – это опасная функция, потому что она создает ложное чувство безопасности..

Поэтому неудивительно, что в настоящее время более 27% веб-сайтов имеют критические уязвимости, в основном XSS (источник: Acunetix Web Vulnerability Report). Как выйти из этой ситуации? Используйте систему шаблонов, которая различает контексты.

Latte – единственная система шаблонизации PHP, которая не воспринимает шаблон как просто строку символов, а понимает HTML. Она знает, что такое теги, атрибуты и т.д. Она различает контексты. И поэтому она правильно экранирует в HTML-тексте, по-разному внутри HTML-тегов, по-разному внутри JavaScript и т.д.

Таким образом, Latte представляет собой единственную безопасную систему шаблонов..


Кроме того, благодаря пониманию HTML, она предлагает замечательные n:attributes, которые нравятся пользователям:.

<ul n:if="$menu">
	<li n:foreach="$menu->getItems() as $item">{$item->title}</li>
</ul>

Последние сообщения