Кодировщик и декодировщик HTML-сущностей
Экранируйте текст, чтобы он был безопасен внутри HTML, или декодируйте именованные и числовые сущности обратно в символы, которые они обозначают.
Здесь появится вывод
Что делает этот инструмент
HTML не умеет отличить знак «меньше», который вы имели в виду как текст, от знака, открывающего тег. Язык решает это сущностями: короткими escape-последовательностями, которые обозначают символ, но не читаются как разметка. Этот инструмент преобразует в обе стороны — обычный текст в экранированный вид и экранированный HTML обратно в символы, которые он обозначает.
Оба направления полностью выполняются в вашем браузере. Здесь это важнее, чем для большинства инструментов: в декодировщик сущностей обычно вставляют собранную парсером страницу, письмо клиента или колонку базы данных, в которой кто-то пытается разобраться.
Пять символов, которые имеют значение
Сущностей тысячи, но чтобы сделать текст безопасным, нужна лишь горстка. Кодирование здесь заменяет ровно пять символов и оставляет всё остальное читаемым:
- Амперсанд становится & — он первым, иначе каждая следующая замена сама была бы прочитана как сущность.
- Знак «меньше» становится < — символ, способный начать тег.
- Знак «больше» становится > — сам по себе менее опасен, экранируется ради симметрии и чтобы пережить небрежные парсеры.
- Двойная кавычка становится " — нужна внутри значений атрибутов, записанных в двойных кавычках.
- Апостроф становится ' — нужен внутри значений атрибутов, записанных в одинарных кавычках.
Последний записан числом намеренно. Имя для него есть, ', но оно пришло из XML, а HTML 4 его так и не определил: старые парсеры пропускают его как буквальный текст, и экранирование молча не срабатывает. Числовая форма всегда работала везде, поэтому серьёзные библиотеки экранирования выдают именно её.
Именованные, десятичные и шестнадцатеричные
Любой символ можно записать тремя способами, и декодирование принимает все. Длинное тире — хороший пример:
named — читаемая форма, но только для символов, у которых есть имя decimal — кодовая точка по основанию 10 hex — та же кодовая точка по основанию 16, запись таблиц Unicode
При декодировании инструмент знает 255 имён набора HTML 4 — латинские буквы, греческий алфавит, стрелки, математические знаки и типографскую пунктуацию, которые вместе покрывают практически всё, что встречается в реальном тексте. HTML 5 определяет ещё около 2200, почти сплошь редкие математические символы; тащить эту таблицу означало бы добавить десятки килобайт к каждой загрузке страницы ради имён, которыми большинство документов никогда не пользуется. Имя вне набора сообщается, а не отбрасывается, так что цена — предупреждение, но никогда не неверный ответ.
Кодирование идёт в обратную сторону и намеренно остаётся простым: имена для пяти символов выше, десятичные числа для всего остального. Ничто не превращается в … или ’, когда сам символ совершенно допустим в документе UTF-8.
Экранирование — это не одно правило, а четыре
Заманчиво считать экранирование сущностей ответом на инъекции, и внутри содержимого элемента оно им действительно является. Но HTML — это на самом деле четыре языка, вложенных друг в друга, и у каждого свои правила. Экранирование сущностей верно в первых двух из этих мест и бесполезно в остальных:
- Содержимое элемента — текст между тегами. Экранирование сущностей — ровно то, что нужно.
- Значения атрибутов в кавычках. Экранирование сущностей подходит, при условии что атрибут действительно в кавычках; без них пробел в значении завершает атрибут, и экранирование вам ничего не даёт.
- Внутри блока script. Там действуют правила экранирования строк JavaScript, а не сущности — браузер там сущности не декодирует, поэтому экранированная кавычка приходит как буквальные символы ".
- Атрибуты с адресом, такие как href и src. Экранирование сущностей не остановит URL вида javascript:, потому что опасность в схеме, а не в символе, который заменила бы сущность.
Практическое правило: экранируйте в той точке, где текст вставляется в документ, по правилам именно того контекста, в который он попадает, — а не один раз, заранее, в надежде, что дальше это сработает везде.
Почему иногда виден &amp;
Это самая частая причина обратиться к декодировщику сущностей. Текст экранировали, а потом экранировали ещё раз — обычно один раз код приложения и один раз шаблонизатор или фреймворк, который и так экранирует свой вывод:
original Fish & Chips escaped Fish & Chips escaped x2 Fish &amp; Chips
Вторая строка отображается на странице как оригинал. Третья отображается как вторая — то есть посетитель видит саму сущность вместо символа. Декодирование снимает ровно один слой, поэтому одного прохода достаточно, чтобы понять, с каким случаем вы имеете дело. Если в выводе всё ещё есть сущности, слоёв было больше одного. Исправление почти никогда не состоит в том, чтобы снимать сущности при выводе: нужно найти и убрать дублирующий шаг экранирования, потому что та же ошибка молча портит и сохранённые данные.
Не-ASCII и режим для него
Странице, отдаваемой в UTF-8, сущности не нужны ни для букв с диакритикой, ни для иврита, арабского, китайского или эмодзи. Эти символы допустимы как есть, а оставить их читаемыми — значит сохранить читаемым исходник и меньшим файл. Так здесь и сделано по умолчанию.
Режим существует потому, что некоторые получатели по-прежнему требуют чистый ASCII: старые почтовые шаблоны, системы, коверкающие байты выше 127, или колонка базы данных, кодировку которой никто не готов менять. При включении каждый такой символ записывается десятичной ссылкой — é становится é, א становится א.
Эмодзи заслуживают отдельного замечания. Эмодзи записывается одной ссылкой на всю свою кодовую точку, например 🙂, и никогда двумя половинами. Если вы пользовались инструментом экранирования 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 оставляют её буквальным текстом, так что апостроф, который вы считали экранированным, таковым не является. Числовая форма работает в любом парсере, поэтому она и выбрана по умолчанию.
- На странице виден &amp; вместо амперсанда. Что произошло?
- Текст экранировали дважды. Декодируйте его здесь: один проход превращает &amp; в &, подтверждая лишний слой. Чините конвейер, а не текст — обычно ваш код экранирует вывод, который шаблонизатор уже экранировал.
- Нужно ли кодировать иврит, арабский или эмодзи?
- Для современной страницы в UTF-8 — нет, они допустимы как есть. Включайте режим не-ASCII только тогда, когда что-то дальше по конвейеру требует чистый ASCII, например старый почтовый шаблон. Каждый эмодзи тогда записывается одной числовой ссылкой.
- Почему инструмент предупреждает о “, если он его нормально разобрал?
- Потому что 147 обозначает невидимый управляющий символ, а не кавычку, которую вы видите. Документы с ним писались в Windows-1252, где 147 — это типографская кавычка: браузеры читают её так, и этот инструмент тоже. Предупреждение сообщает, что произошла подстановка, чтобы вы могли исправить исходную кодировку, если это в вашей власти.
- Отправляется ли мой текст куда-нибудь?
- Нет. Кодирование и декодирование полностью выполняются в вашем браузере; ничто из вставленного не покидает ваше устройство.