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

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

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

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

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

Первые три вопроса

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

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

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

$str = 'foo" onclick="evilCode()';
❌ без обработки: <input value="foo" onclick="evilCode()">
✅ с обработкой:  <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 не поддерживает многострочные строки (только как template literal), мы должны экранировать и символы конца строк. Однако будьте осторожны, кроме обычных символов \n и \r, JavaScript считает концами строк и unicode-символы \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-странице, то знайте, что мы упомянули далеко не все. Это была бы гораздо более длинная викторина. Тем не менее, вам не нужно быть экспертом по экранированию в каждом контексте, если с этим справляется ваша система шаблонов.

Давайте же их протестируем.

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

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

Мы протестируем популярные системы шаблонов на вопросах этой викторины, чтобы выяснить, насколько эффективно их автоэкранирование. Начинается dTest систем шаблонов для 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 провалил шесть из девяти тестов!

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

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

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

Blade ❌❌

Вторым участником является система шаблонов Blade (версия 10.9), которая тесно интегрирована с Laravelem и его экосистемой. Снова проверим ее способности на наших вопросах викторины. Ее ответы вы также можете изучить на песочнице.

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

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

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

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

Smarty ❌❌❌

Теперь протестируем старейшую систему шаблонов для PHP — Smarty (версия 4.3). К большому удивлению, у этой системы нет активного автоматического экранирования. Вам нужно либо каждый раз при выводе переменных указывать фильтр {$var|escape}, либо активировать автоматическое экранирование HTML. Информация об этом в документации довольно запрятана.

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

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

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

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

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

   {$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;\u2028; </script>
❌ <p onclick="foo(&#039;&quot;\u2028)"></p>


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


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


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

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

Результат на первый взгляд похож на предыдущие библиотеки. Smarty умеют автоматически экранировать только в HTML-тексте и атрибутах, и то только когда значения заключены в кавычки. Везде остальное они дают сбой. В контекстах (5) и (6) необходимо экранировать вручную с помощью {$str|escape:javascript}. Но это возможно только тогда, когда не активно автоматическое экранирование HTML, иначе эти экранирования конфликтуют друг с другом. Smarty, таким образом, с точки зрения безопасности — полный провал этого теста.

Latte ✅

Тройку замыкает система шаблонов 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>

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

Он справился с отсутствующими кавычками у 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 '> ❌

Blade: провал ❌ при выводе блоков символы не обрабатывает

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

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




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

Latte: успех ✅ при выводе блоков корректно обработал проблемные символы

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

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