Menu

PHP htmlspecialchars(): экранирование HTML и защита от XSS

htmlspecialchars($text) превращает пять символов, которые что-то значат в HTML (& < > " '), в сущности, поэтому пользовательский ввод показывается как текст, а не выполняется как разметка. ENT_QUOTES, экранирование атрибутов, двойное кодирование, htmlentities и strip_tags.

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

htmlspecialchars($text) преобразует &, <, >, " и ' в &amp;, &lt;, &gt;, &quot; и &#039;. Вызывайте её для каждого фрагмента пользовательского ввода, который выводите на страницу, и ввод будет показан как текст, а не прочитан как HTML.

Блок выводит один и тот же комментарий дважды, один раз как есть и один раз экранированным. Запустите его и сравните две строки во вкладке Page, затем введите в форму свой HTML, например <h1>big</h1> или <img src=x>, и нажмите Show.

В строке без экранирования браузер подчиняется тегам: жирное слово жирное, а посетитель, который ввёл <script>, получает свой скрипт выполненным в браузере у каждого другого читателя. Эта атака называется межсайтовым скриптингом (XSS). В экранированной строке те же символы приходят как &lt;b&gt;, и браузер рисует их как текст. Переключитесь на вкладку Output, чтобы увидеть сущности, которые на самом деле вывел PHP.

Что преобразует htmlspecialchars

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

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

Экранируйте атрибуты, а не только текст

Пользовательский ввод внутри атрибута нуждается в экранировании ничуть не меньше. Без него кавычка в значении закрывает атрибут, а остаток ввода становится новыми атрибутами. Здесь "имя" протаскивает атрибут style; запустите и посмотрите на два поля.

В небезопасном поле браузер видит value="Ada", а за ним новый атрибут style, поэтому поле становится красным и показывает только Ada. Атакующий написал бы там onfocus="..." вместо style, и его код выполнился бы. В безопасном поле каждая " стала &quot;, поэтому вся строка остаётся внутри value и показывается так, как введена.

Начиная с PHP 8.1 флаги по умолчанию это ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, поэтому одинарные кавычки тоже экранируются, и атрибуты, записанные через '...', безопасны. Старый код часто передаёт ENT_QUOTES вручную, а в PHP 7 и раньше это было обязательно:

Короткая вспомогательная функция для шаблонов

Писать htmlspecialchars($x, ENT_QUOTES, 'UTF-8') десятки раз в шаблоне громоздко, поэтому большинство проектов оборачивают её в функцию из одной буквы. Шаблонизаторы вроде Twig и Blade делают то же автоматически для каждого {{ $var }}.

Тип ?string и ?? '' важны: передача null в htmlspecialchars() объявлена устаревшей с PHP 8.1, и ваш PHP печатал бы уведомление об устаревании для каждого пользователя без описания.

Двойное кодирование и htmlspecialchars_decode

Если значение экранировано дважды, читатель видит сущности: & в первый раз становится &amp;, а во второй &amp;amp;, что браузер показывает как &amp;. Обычно это значит, что значение экранировали при сохранении и снова при выводе. Передайте double_encode: false, чтобы не трогать существующие сущности, а для обратного преобразования используйте htmlspecialchars_decode().

Настоящее решение это хранить исходный текст и экранировать только при выводе. double_encode: false нужен для текста, где уже есть сущности, созданные не вами, например в импортированной ленте.

htmlspecialchars, htmlentities и strip_tags

Эти три функции часто путают. Блок прогоняет все три на одних и тех же данных и показывает для каждой, что выводит PHP и что из этого делает браузер:

  • htmlspecialchars() экранирует пять символов HTML. Используйте её для любого текста, который выводите в HTML.
  • htmlentities() превращает ещё и é в &eacute;. Это было полезно, когда страницы были не в UTF-8; сегодня это лишь затрудняет чтение исходного кода.
  • strip_tags() удаляет теги и оставляет их текст. Она нужна, чтобы превратить HTML в простой текст (превью письма, meta description), а не для безопасности: последняя строка показывает, что разрешённый <b> сохраняет свой onclick, а текст внутри атрибута не трогается вовсе.

Где htmlspecialchars недостаточно

htmlspecialchars() это правильное экранирование для текста HTML и атрибутов в кавычках. В других местах страницы свои правила:

  • В URL значение кодирует http_build_query() или urlencode(); затем htmlspecialchars() делает & между параметрами корректным HTML.
  • В JavaScript json_encode() даёт корректное значение JS, а JSON_HEX_TAG превращает < и > в \u003C и \u003E, поэтому </script> в данных не может закрыть тег скрипта.
  • Никогда не выводите пользовательский ввод в href, не проверив схему: htmlspecialchars('javascript:alert(1)') остаётся без изменений и всё равно выполняется при нажатии. Принимайте только URL с http и https, как показано на странице про filter_var.

Обработка форм, которая собирает всё это вместе, описана в разделе о формах PHP.

Часто задаваемые вопросы

Что делает htmlspecialchars в PHP?

Заменяет &, <, >, " и ' на &amp;, &lt;, &gt;, &quot; и &#039;. Тогда браузер показывает эти символы, а не читает их как HTML, поэтому <script>, введённый в форму, отображается как текст и никогда не выполняется.

Чем htmlspecialchars отличается от htmlentities?

htmlspecialchars() преобразует только пять символов, особых в HTML. htmlentities() преобразует ещё и каждый символ, у которого есть именованная сущность, поэтому café становится caf&eacute;. На страницах в UTF-8 обе одинаково безопасны, а htmlspecialchars() оставляет вывод читаемым, поэтому её и выбирают обычно.

Нужен ли ENT_QUOTES в PHP 8?

Для безопасности нет: начиная с PHP 8.1 флаги по умолчанию это ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, поэтому одинарные кавычки тоже экранируются. Во многих проектах всё равно явно передают ENT_QUOTES, 'UTF-8', чтобы вызов вёл себя одинаково в старых версиях и был очевиден читателю.

Применять htmlspecialchars при вводе или при выводе?

При выводе. Храните и проверяйте исходное значение, а экранируйте в момент вывода в HTML. Экранирование при вводе сохраняет &lt; в базе данных, ломает длины и поиск и приводит к двойному экранированию вроде &amp;lt;.

Достаточно ли strip_tags для защиты от XSS?

Нет. strip_tags() удаляет теги, но её параметр разрешённых тегов сохраняет их атрибуты, поэтому <b onclick="..."> выживает, и она ничего не делает с текстом внутри атрибута. При выводе пользовательского ввода используйте htmlspecialchars().

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

НАЧАТЬ