Совет по безопасности: Экранирование с e(), htmlspecialchars() и htmlentities()

Источник: «Security Tip: Escape Output with e(), htmlspecialchars(), & htmlentities()!»
[Совет#64] Знаете ли вы разницу между e(), htmlspecialchars() и htmlentities()? Можно ли просто использовать e() для всего?

На прошлой неделе мы рассмотрели, когда следует использовать strip_tags() (спойлер: только при выводе вне каких-либо атрибутов или сложных структур), а теперь пришло время рассмотреть другие методы из этой коллекции: htmlspecialchars() и htmlentities(). Давайте также включим в список метод Laravel e(), потому что он тесно связан с ним — я объясню, почему.

Чтобы объяснить, что делают эти функции, я закодирую одну и ту же строку в обеих, и вы сможете сравнить полученные результаты.

> $string = 'Hello <img src="x" onerror="alert(\'Boom!\')"> World!';
= "Hello <img src="x" onerror="alert('Boom!')"> World!"

> htmlspecialchars($string);
= "Hello &lt;img src=&quot;x&quot; onerror=&quot;alert(&#039;Boom!&#039;)&quot;&gt; World!"

> htmlentities($string);
= "Hello &lt;img src=&quot;x&quot; onerror=&quot;alert(&#039;Boom!&#039;)&quot;&gt; World!"

Замечаете различия? Нет, я тоже не заметил.

На самом деле, разница заметна только тогда, когда мы используем дополнительные символы:

> $string = " ¡¢£¥§©«®°¶·»¼½¾¿™";
= " ¡¢£¥§©«®°¶·»¼½¾¿™"

> htmlspecialchars($string);
= " ¡¢£¥§©«®°¶·»¼½¾¿™"

> htmlentities($string);
= " &iexcl;&cent;&pound;&yen;&sect;&copy;&laquo;&reg;&deg;&para;&middot;&raquo;&frac14;&frac12;&frac34;&iquest;&trade;"

htmlentities() будет кодировать каждый специальный символ соответствующей HTML сущностью, в то время как htmlspecialchars() будет кодировать только основное подмножество, которое обычно используется для атак межсайтового скриптинга (XSS) в HTML выводе: & " ' < >

Обратите внимание, что этот список не является исчерпывающим или идеальным — есть ситуации, в которых он не подходит. Я расскажу об этом в конце.

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

Например, htmlspecialchars() кодирует такие символы:

Так как же e() относится к этому?

Среди нескольких других помощников, метод Laravel e() использует htmlspecialchars() и является методом экранирования, повсеместно используемым Laravel. Laravel также устанавливает некоторые разумные значения по умолчанию и предоставляет обёртку с некоторыми дополнительными функциями, которые мы можем использовать. Это делает e() пригодным для использования в большинстве мест (см. ниже), где требуется экранирование в HTML-выводе.

Я рекомендую использовать e() всегда, когда вам нужно вручную экранировать вывод в HTML.

Тем не менее вы можете использовать любой из этих трёх методов, если это имеет смысл. Мне нравится e() из-за его дополнительных функций и краткости, но htmlspecialchars() тоже справляется с этой задачей, а если вам нужно закодировать дополнительные символы, то следует обратиться к htmlentities().

Почему же мы не видим e() в шаблонах Blade? Технически это так! Экранирующие теги Blade, {{ ... }}, используют e() в фоновом режиме для экранирования вывода, делая его безопасным. Поэтому обращаться к e() или другим тегам нужно только за пределами Blade или в сложных структурах, которые нельзя вывести с помощью {{ ... }}.

Важное обновление (7 декабря)

В первоначальной версии этой статьи подразумевалось, что e() и htmlspecialchars() безопасно использовать для экранирования вывода в любом месте. Это попросту не так, и мне не следовало так формулировать статью. (Огромная благодарность Paul Moore за то, что он меня в этом разубедил!)

Экранирование вывода всегда требует понимания контекста, в который он выводится. Если вы выводите в HTML вне тегов, то < > & — ваша самая большая проблема, внутри атрибутов вам придётся иметь дело с " '. Что касается строк шаблонов Javascript — внезапно у нас появляются обратные кавычки ( ` ) и знаки доллара ( $ ), которые тоже нужно учитывать. Как насчёт пользовательского шаблонизатора, который, возможно, ищет {{ ... }}, которые можно включить в пользовательский вывод, чтобы обойти Blade…

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

Прежде чем мы закончим, я хотел бы сделать небольшое замечание о PHP 8.0 и более ранних версиях!

В PHP 8.0 и более ранних версиях htmlspecialchars() и htmlentities() игнорировали одинарные кавычки и экранировали только двойные кавычки в соответствии с флагом по умолчанию ENT_COMPAT. Это значение было изменено в PHP 8.1 на ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, который по умолчанию кодирует оба типа кавычек.

Это открывает XSS-возможности при использовании одинарных кавычек в HTML, куда внедряются переменные, и поэтому, если в вашей среде используется PHP 8.0 или более ранняя версия, вы не сможете безопасно использовать htmlspecialchars() или htmlentities(), если не установите флаг ENT_QUOTES вручную.

Обратите внимание, что e() включает этот флаг с тех пор, как этот хелпер впервые был добавлен во фреймворк, 11 лет назад — так что я рекомендую использовать e().

Узнать больше

  1. PHP: strip_tags
  2. PHP: htmlspecialchars - Manual
  3. PHP: htmlentities - Manual
  4. e()
  5. Полную функцию можно найти в: src/Illuminate/Support/helpers.php. Мы уже рассказывали об интерфейсе Htmlable ранее, в статье Laravel Security In Depth: Escaping Output Safely и в статье Security Tip: Avoiding XSS with HtmlString.

Дополнительные материалы

Предыдущая Статья

Не полагайтесь на порядок ключей в значениях MySQL JSON

Следующая Статья

Laravel под капотом: CSRF