Кодировщик и декодировщик HTML-сущностей

Экранируйте текст, чтобы он был безопасен внутри HTML, или декодируйте именованные и числовые сущности обратно в символы, которые они обозначают.

Текст
Закодировано

Здесь появится вывод

Что делает этот инструмент

HTML не умеет отличить знак «меньше», который вы имели в виду как текст, от знака, открывающего тег. Язык решает это сущностями: короткими escape-последовательностями, которые обозначают символ, но не читаются как разметка. Этот инструмент преобразует в обе стороны — обычный текст в экранированный вид и экранированный HTML обратно в символы, которые он обозначает.

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

Пять символов, которые имеют значение

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

  • Амперсанд становится & — он первым, иначе каждая следующая замена сама была бы прочитана как сущность.
  • Знак «меньше» становится < — символ, способный начать тег.
  • Знак «больше» становится > — сам по себе менее опасен, экранируется ради симметрии и чтобы пережить небрежные парсеры.
  • Двойная кавычка становится " — нужна внутри значений атрибутов, записанных в двойных кавычках.
  • Апостроф становится ' — нужен внутри значений атрибутов, записанных в одинарных кавычках.

Последний записан числом намеренно. Имя для него есть, ', но оно пришло из XML, а HTML 4 его так и не определил: старые парсеры пропускают его как буквальный текст, и экранирование молча не срабатывает. Числовая форма всегда работала везде, поэтому серьёзные библиотеки экранирования выдают именно её.

Именованные, десятичные и шестнадцатеричные

Любой символ можно записать тремя способами, и декодирование принимает все. Длинное тире — хороший пример:

named     —      читаемая форма, но только для символов, у которых есть имя
decimal   —      кодовая точка по основанию 10
hex       —     та же кодовая точка по основанию 16, запись таблиц Unicode

При декодировании инструмент знает 255 имён: все, которые определяет HTML 4, плюс три, которые он не определяет, — ', пришедшее из XML, и Zcaron с zcaron, два знака Windows-1252, оставшиеся в HTML 4 без имени. Вместе они покрывают латинские буквы, греческий алфавит, стрелки, математические знаки и типографскую пунктуацию — практически всё, что встречается в реальном тексте. HTML 5 определяет около 2100 имён в целом, а остальные из них — почти сплошь редкие математические символы; тащить эту таблицу означало бы добавить десятки килобайт к каждой загрузке страницы ради имён, которыми большинство документов никогда не пользуется. Имя вне набора сообщается, а не отбрасывается, так что цена — предупреждение, но никогда не неверный ответ.

Кодирование идёт в обратную сторону и намеренно остаётся простым: имена для пяти символов выше, десятичные числа для всего остального. Ничто не превращается в … или ’, когда сам символ совершенно допустим в документе UTF-8.

Экранирование — это не одно правило, а четыре

Заманчиво считать экранирование сущностей ответом на инъекции, и внутри содержимого элемента оно им действительно является. Но HTML — это на самом деле четыре языка, вложенных друг в друга, и у каждого свои правила. Экранирование сущностей верно в первых двух из этих мест и бесполезно в остальных:

  • Содержимое элемента — текст между тегами. Экранирование сущностей — ровно то, что нужно.
  • Значения атрибутов в кавычках. Экранирование сущностей подходит, при условии что атрибут действительно в кавычках; без них пробел в значении завершает атрибут, и экранирование вам ничего не даёт.
  • Внутри блока script. Там действуют правила экранирования строк JavaScript, а не сущности — браузер там сущности не декодирует, поэтому экранированная кавычка приходит как буквальные символы ".
  • Атрибуты с адресом, такие как href и src. Экранирование сущностей не остановит URL вида javascript:, потому что опасность в схеме, а не в символе, который заменила бы сущность.

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

Почему иногда виден &

Это самая частая причина обратиться к декодировщику сущностей. Текст экранировали, а потом экранировали ещё раз — обычно один раз код приложения и один раз шаблонизатор или фреймворк, который и так экранирует свой вывод:

original     Fish & Chips
escaped      Fish & Chips
escaped x2   Fish & Chips

Вторая строка отображается на странице как оригинал. Третья отображается как вторая — то есть посетитель видит саму сущность вместо символа. Декодирование снимает ровно один слой, поэтому одного прохода достаточно, чтобы понять, с каким случаем вы имеете дело. Если в выводе всё ещё есть сущности, слоёв было больше одного. Исправление почти никогда не состоит в том, чтобы снимать сущности при выводе: нужно найти и убрать дублирующий шаг экранирования, потому что та же ошибка молча портит и сохранённые данные.

Не-ASCII и режим для него

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

Режим существует потому, что некоторые получатели по-прежнему требуют чистый ASCII: старые почтовые шаблоны, системы, коверкающие байты выше 127, или колонка базы данных, кодировку которой никто не готов менять. При включении такой символ записывается десятичной ссылкой везде, где эта ссылка читается обратно как тот же самый символ, — é становится é, א становится א. Там, где так надёжно не прочиталась бы ни одна ссылка, символ остаётся ровно таким, каким был, вместо замены на ссылку, которая значит другое: это числа от 128 до 159, которые HTML либо читает как описанные ниже символы Windows-1252, либо помечает как ошибку, но никогда — как управляющие символы, названные этими числами, и одинокая половина пары, оставшаяся от уже испорченного текста, — её не называет никакая ссылка.

Эмодзи заслуживают отдельного замечания. Эмодзи записывается одной ссылкой на всю свою кодовую точку, например 🙂, и никогда двумя половинами. Если вы пользовались инструментом экранирования JSON, там всё наоборот: формат прямо требует разбить эмодзи на две escape-последовательности \u. Один и тот же символ, два правильных ответа, потому что два формата определяют экранирование над разными единицами.

Когда ссылку не удаётся разобрать

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

  • Неизвестное имя, например &nbps; — почти всегда опечатка вместо  . Это предупреждение, на которое стоит отреагировать: в вашем тексте не хватает пробела, и этого никто не заметил.
  • Число, которое не является символом, например � или значение выше диапазона Unicode. Разобрать его не во что, поэтому оно остаётся текстом.
  • Число от 128 до 159. См. ниже.

Одиночный амперсанд не сообщается вовсе. В HTML R&D и Q&A — обычный текст, и если считать ошибкой каждый случайный амперсанд, важные предупреждения утонут в шуме от обычной прозы.

Диапазон от 128 до 159 — настоящая странность. Эти числа обозначают невидимые управляющие символы, которые никто никогда не имеет в виду; они появляются потому, что документ готовили в Windows-1252, где те же числа — это типографские кавычки, тире и знак евро. Браузеры именно поэтому негласно читают их как Windows-1252, и инструмент поступает так же, чтобы результат совпадал с тем, что страница покажет на самом деле, — но говорит об этом в предупреждениях, потому что тихая подстановка, о которой вы не просили, хуже громкой.

Частые вопросы

Достаточно ли экранировать эти пять символов, чтобы остановить XSS?
В содержимом элемента и в значениях атрибутов в кавычках — да, именно для этого оно и нужно. Этого недостаточно внутри блока script или style, в атрибуте без кавычек и в атрибуте с адресом вроде href: каждому из этих мест нужны свои правила. Экранирование — свойство места, в которое попадает текст, а не самого текста.
Почему апостроф кодируется как ', а не как '?
Потому что ' — это XML-сущность, которую HTML 4 никогда не определял. Парсеры до HTML 5 оставляют её буквальным текстом, так что апостроф, который вы считали экранированным, таковым не является. Числовая форма работает в любом парсере, поэтому она и выбрана по умолчанию.
На странице виден & вместо амперсанда. Что произошло?
Текст экранировали дважды. Декодируйте его здесь: один проход превращает & в &, подтверждая лишний слой. Чините конвейер, а не текст — обычно ваш код экранирует вывод, который шаблонизатор уже экранировал.
Нужно ли кодировать иврит, арабский или эмодзи?
Для современной страницы в UTF-8 — нет, они допустимы как есть. Включайте режим не-ASCII только тогда, когда что-то дальше по конвейеру требует чистый ASCII, например старый почтовый шаблон. Каждый эмодзи тогда записывается одной числовой ссылкой.
Почему инструмент предупреждает о “, если он его нормально разобрал?
Потому что 147 обозначает невидимый управляющий символ, а не кавычку, которую вы видите. Документы с ним писались в Windows-1252, где 147 — это типографская кавычка: браузеры читают её так, и этот инструмент тоже. Предупреждение сообщает, что произошла подстановка, чтобы вы могли исправить исходную кодировку, если это в вашей власти.
Отправляется ли мой текст на сервер?
Нет. Кодирование и декодирование полностью выполняются в вашем браузере; ничто из вставленного не покидает ваше устройство.

Похожие инструменты