htmlspecialchars($text) преобразует &, <, >, " и ' в &, <, >, " и '. Вызывайте её для каждого фрагмента пользовательского ввода, который выводите на страницу, и ввод будет показан как текст, а не прочитан как HTML.
Блок выводит один и тот же комментарий дважды, один раз как есть и один раз экранированным. Запустите его и сравните две строки во вкладке Page, затем введите в форму свой HTML, например <h1>big</h1> или <img src=x>, и нажмите Show.
В строке без экранирования браузер подчиняется тегам: жирное слово жирное, а посетитель, который ввёл <script>, получает свой скрипт выполненным в браузере у каждого другого читателя. Эта атака называется межсайтовым скриптингом (XSS). В экранированной строке те же символы приходят как <b>, и браузер рисует их как текст. Переключитесь на вкладку Output, чтобы увидеть сущности, которые на самом деле вывел PHP.
Что преобразует htmlspecialchars
Пять символов и ничего больше. Буквы, диакритика и эмодзи проходят без изменений.
& в этом списке, потому что с него начинается каждая сущность: если оставить его как есть, комментарий, где упоминается <, отобразился бы как <.
Экранируйте атрибуты, а не только текст
Пользовательский ввод внутри атрибута нуждается в экранировании ничуть не меньше. Без него кавычка в значении закрывает атрибут, а остаток ввода становится новыми атрибутами. Здесь "имя" протаскивает атрибут style; запустите и посмотрите на два поля.
В небезопасном поле браузер видит value="Ada", а за ним новый атрибут style, поэтому поле становится красным и показывает только Ada. Атакующий написал бы там onfocus="..." вместо style, и его код выполнился бы. В безопасном поле каждая " стала ", поэтому вся строка остаётся внутри 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;, что браузер показывает как &. Обычно это значит, что значение экранировали при сохранении и снова при выводе. Передайте double_encode: false, чтобы не трогать существующие сущности, а для обратного преобразования используйте htmlspecialchars_decode().
Настоящее решение это хранить исходный текст и экранировать только при выводе. double_encode: false нужен для текста, где уже есть сущности, созданные не вами, например в импортированной ленте.
htmlspecialchars, htmlentities и strip_tags
Эти три функции часто путают. Блок прогоняет все три на одних и тех же данных и показывает для каждой, что выводит PHP и что из этого делает браузер:
htmlspecialchars()экранирует пять символов HTML. Используйте её для любого текста, который выводите в HTML.htmlentities()превращает ещё иéвé. Это было полезно, когда страницы были не в 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?
Заменяет &, <, >, " и ' на &, <, >, " и '. Тогда браузер показывает эти символы, а не читает их как HTML, поэтому <script>, введённый в форму, отображается как текст и никогда не выполняется.
Чем htmlspecialchars отличается от htmlentities?
htmlspecialchars() преобразует только пять символов, особых в HTML. htmlentities() преобразует ещё и каждый символ, у которого есть именованная сущность, поэтому café становится café. На страницах в UTF-8 обе одинаково безопасны, а htmlspecialchars() оставляет вывод читаемым, поэтому её и выбирают обычно.
Нужен ли ENT_QUOTES в PHP 8?
Для безопасности нет: начиная с PHP 8.1 флаги по умолчанию это ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, поэтому одинарные кавычки тоже экранируются. Во многих проектах всё равно явно передают ENT_QUOTES, 'UTF-8', чтобы вызов вёл себя одинаково в старых версиях и был очевиден читателю.
Применять htmlspecialchars при вводе или при выводе?
При выводе. Храните и проверяйте исходное значение, а экранируйте в момент вывода в HTML. Экранирование при вводе сохраняет < в базе данных, ломает длины и поиск и приводит к двойному экранированию вроде &lt;.
Достаточно ли strip_tags для защиты от XSS?
Нет. strip_tags() удаляет теги, но её параметр разрешённых тегов сохраняет их атрибуты, поэтому <b onclick="..."> выживает, и она ничего не делает с текстом внутри атрибута. При выводе пользовательского ввода используйте htmlspecialchars().