HTML-сущности: когда экранировать, а когда нет
Кодирование HTML-сущностей — одна из тех тем, которые кажутся простыми, пока вам не нужно решить, кодировать ли пользовательский ввод, попадающий в HTML-атрибут, текстовое содержимое, URL-параметр или JSON-данные, встроенные в тег <script>. Ошибка открывает дверь для атак межсайтового скриптинга (XSS). Слишком агрессивное кодирование ломает легитимный контент.
Это руководство объясняет, когда кодирование HTML-сущностей необходимо для безопасности, когда оно необязательно и как безопасно обрабатывать данные в разных HTML-контекстах, не нарушая функциональность и не создавая уязвимостей.
Что такое HTML-сущности?
HTML-сущности — это специальные последовательности, представляющие зарезервированные символы в HTML. Они начинаются с & и заканчиваются ;.
| Символ | Именованная сущность | Числовая сущность | Зачем кодировать |
|---|---|---|---|
< | < | < | Начинает HTML-тег |
> | > | > | Заканчивает HTML-тег |
& | & | & | Начинает сущность |
" | " | " | Разделитель атрибута |
' | ' | ' | Разделитель атрибута |
Браузер автоматически декодирует эти сущности при рендеринге страницы. Если вы видите <script> в HTML-коде, браузер отображает <script> как обычный текст, а не выполняет его как тег.
Контекст имеет значение: где нужно кодирование
Золотое правило: требования к кодированию зависят от контекста. HTML имеет несколько контекстов, где могут появляться данные, и у каждого свои правила экранирования.
Контекст 1: Текстовое содержимое HTML
Текстовое содержимое между тегами (не внутри атрибутов) требует кодирования для <, > и &:
<p>Комментарий пользователя: <script>alert('XSS')</script></p>
<!-- Уязвимо: браузер выполняет скрипт -->
<p>Комментарий пользователя: <script>alert('XSS')</script></p>
<!-- Безопасно: браузер отображает как текст -->Обязательное кодирование: < → <, > → >, & → &
Необязательно: Кавычки (" и ') не требуют кодирования в текстовом содержимом, потому что они не имеют особого значения вне атрибутов.
Контекст 2: HTML-атрибуты
Атрибуты требуют кодирования для " (или ', если используются одинарные кавычки):
<input value="Ввод пользователя: " onclick="alert(1)">
<!-- Уязвимо: атакующий внедряет атрибут onclick -->
<input value="Ввод пользователя: " onclick="alert(1)">
<!-- Безопасно: кавычки закодированы, нет внедрения атрибута -->Обязательное кодирование в атрибутах с двойными кавычками: " → ", & → &
Обязательное кодирование в атрибутах с одинарными кавычками: ' → ' или ', & → &
Контекст 3: JavaScript внутри HTML
Встраивание данных в теги <script> — самый опасный контекст. Кодирование HTML-сущностей здесь не помогает, потому что браузер парсит содержимое скрипта как JavaScript, а не как HTML.
<script>
const userName = "<script>alert(1)</script>";
</script>
<!-- HTML-сущности декодируются ДО выполнения JavaScript -->
<!-- Результат: const userName = "<script>alert(1)</script>"; -->
<!-- По-прежнему уязвимо для внедрения скрипта -->Безопасный подход: Используйте JSON-кодирование и правильное экранирование:
<script>
const userName = ${JSON.stringify(userInput)};
</script>
<!-- Безопасно: JSON.stringify экранирует кавычки и спецсимволы -->Однако даже JSON.stringify недостаточно, если строка содержит </script>:
<script>
const data = "</script><script>alert(1)</script>";
</script>
<!-- Выходит из script-тега и выполняет внедренный код -->Полностью безопасный подход: Экранируйте последовательности </script>:
<script>
const data = ${JSON.stringify(userInput).replace(/</g, '\u003c')};
</script>
<!-- Безопасно: < экранирован как Unicode \u003c -->Контекст 4: URL-атрибуты
URL в атрибутах href или src требуют другой обработки:
<a href="javascript:alert(1)">Нажми</a>
<!-- Опасно: выполняет JavaScript -->
<a href="https://example.com?q=ввод пользователя">Ссылка</a>
<!-- Требует URL-кодирования, а не кодирования HTML-сущностей -->Для URL используйте URL-кодирование (процентное кодирование) сначала, затем кодирование HTML-сущностей для контекста атрибута:
const safeUrl = "https://example.com?q=" + encodeURIComponent(userInput);
// Затем встраивайте в HTML-атрибут с кодированием сущностей для кавычекИспользуйте URL-кодировщик для обработки значений строки запроса, затем примените кодирование HTML-сущностей при встраивании полного URL в атрибут.
Защита от XSS: реальные сценарии атак
Атака 1: Выход из атрибутов
Представьте форму поиска, которая отображает запрос пользователя:
<input type="text" value="${userQuery}">
// Если userQuery = "><script>alert(1)</script>
// Результат:
<input type="text" value=""><script>alert(1)</script>">Исправление: Кодируйте кавычки в значениях атрибутов:
<input type="text" value=""><script>alert(1)</script>">
// Безопасно: отображается как обычный текстАтака 2: JSON в HTML-контексте
Разработчики часто встраивают JSON-конфигурацию в HTML:
<script>
const config = {"name": "${userName}"};
</script>
// Если userName = Алиса", "admin": true, "name": "
// Результат:
const config = {"name": "Алиса", "admin": true, "name": ""};
// Атакующий внедряет JSON-ключиИсправление: Всегда используйте JSON.stringify и экранируйте </script>:
<script>
const config = ${JSON.stringify({name: userName}).replace(/</g, '\u003c')};
</script>Атака 3: Внедрение обработчика событий
<div title="${userInput}">Наведи на меня</div>
// Если userInput = " onmouseover="alert(1)
// Результат:
<div title="" onmouseover="alert(1)">Наведи на меня</div>Исправление: Кодируйте кавычки в атрибутах:
<div title="" onmouseover="alert(1)">Наведи на меня</div>
// Безопасно: кавычки — буквальный текст, не разделители атрибутовКогда кодирование необязательно
Статический контент
Если содержимое жестко закодировано и не контролируется пользователем, кодирование необязательно (хотя все равно хорошая практика):
<p>Тег <code> используется для кода.</p>
<!-- Работает нормально, нет пользовательского ввода -->
<p>Тег <code> используется для кода.</p>
<!-- Тоже нормально, более явно -->Предварительно очищенный HTML
Если вы намеренно рендерите HTML (например, из визуального редактора) после очистки доверенной библиотекой, вы не должны кодировать сущности, потому что это сломает структуру HTML:
const sanitizedHTML = DOMPurify.sanitize(userHTML);
// НЕ кодируйте сущности после санитизации
document.innerHTML = sanitizedHTML;
// Если закодируете:
document.innerHTML = htmlEncode(sanitizedHTML);
// Результат: HTML-теги отображаются как текст, не рендерятсяJSON-ответы
JSON-ответы API не требуют кодирования HTML-сущностей, потому что они не парсятся как HTML:
// Ответ API (Content-Type: application/json)
{
"comment": "<script>alert(1)</script>"
}
// Безопасно: JSON — не HTML, нет парсинга как тегов
// HTML-кодирование повредит данныеРаспространённые ошибки
Ошибка 1: Слишком раннее кодирование
Кодирование данных при сохранении в базу данных вместо рендеринга вызывает повреждение:
// Неправильно: кодировать перед сохранением
db.save({ name: htmlEncode(userName) });
// Результат: база данных содержит "<Алиса>" вместо "<Алиса>"
// Если позже отправить это в JSON API, клиенты получат поврежденные данные
// Правильно: сохранять сырым, кодировать при рендеринге HTML
db.save({ name: userName });
// При рендеринге:
<p>${htmlEncode(data.name)}</p>Ошибка 2: Использование HTML-кодирования для URL
// Неправильно: HTML-кодирование не работает для URL
<a href="/search?q=${htmlEncode('привет мир')}">Ссылка</a>
// Результат: /search?q=привет мир
// Сломано: пробел не закодирован для URL
// Правильно: используйте URL-кодирование
<a href="/search?q=${encodeURIComponent('привет мир')}">Ссылка</a>
// Результат: /search?q=%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%BC%D0%B8%D1%80Используйте URL-кодировщик для параметров запроса и HTML-кодировщик для текстового содержимого и атрибутов.
Ошибка 3: Доверие только клиентскому кодированию
Кодирование на клиенте не защищает сервер. Атакующие могут обойти клиентский код и отправить сырые запросы:
// Клиентское кодирование:
const encoded = htmlEncode(userInput);
fetch('/api/comment', { body: JSON.stringify({ text: encoded }) });
// Атакующий обходит клиент, отправляет сырой запрос:
POST /api/comment
{"text": "<script>alert(1)</script>"}
// Сервер должен кодировать при рендеринге HTMLВсегда кодируйте на сервере при генерации HTML.
Лучшие практики для безопасного рендеринга HTML
1. Кодируйте в последний момент
Храните сырые данные, кодируйте только при рендеринге HTML. Это сохраняет данные чистыми для других использований (API-ответы, экспорты, поиск).
2. Используйте кодирование по контексту
- Текстовое содержимое HTML: Кодируйте
<,>,& - HTML-атрибуты: Кодируйте
"(или') и& - JavaScript-контекст: Используйте
JSON.stringify+ экранируйте</script> - URL-контекст: Используйте URL-кодирование (процентное кодирование)
3. Используйте встроенные функции фреймворков
Современные фреймворки обрабатывают кодирование автоматически:
// React: автоматически кодирует текстовое содержимое и атрибуты
<div title={userInput}>{userInput}</div>
// Vue: то же самое автокодирование
<div :title="userInput">{{ userInput }}</div>
// Angular: то же самое
<div [title]="userInput">{{ userInput }}</div>Обходите кодирование фреймворка только если намеренно рендерите очищенный HTML с dangerouslySetInnerHTML или эквивалентом.
4. Тестируйте со специальными символами
Во время разработки тестируйте с вводами, содержащими <>"'& и </script>, чтобы проверить, что кодирование работает правильно. Используйте HTML-кодировщик/декодер для генерации тестовых данных и проверки их рендеринга.
5. Валидируйте ввод, кодируйте вывод
Валидация ввода отклоняет некорректные данные (например, проверка формата email). Кодирование вывода предотвращает XSS при рендеринге. Оба необходимы:
- Валидация предотвращает попадание плохих данных в систему
- Кодирование предотвращает выполнение сохраненных данных как кода при отображении
Инструменты для кодирования HTML-сущностей
При отладке или ручном создании HTML необходим специальный HTML-кодировщик/декодер. С помощью DevToys Pro HTML-кодировщика/декодера вы можете:
- Кодировать пользовательский ввод, чтобы увидеть, как он должен выглядеть в HTML
- Декодировать HTML-код для проверки исходных данных
- Тестировать крайние случаи с кавычками, амперсандами и угловыми скобками
- Проверять, закодированы ли данные дважды или повреждены
- Генерировать тестовые данные для тестирования безопасности
Краткая справка: правила кодирования по контексту
| Контекст | Обязательное кодирование | Инструмент |
|---|---|---|
| Текстовое содержимое HTML | < > & | HTML-кодировщик |
| HTML-атрибуты | " ' & | HTML-кодировщик |
JavaScript <script> | JSON.stringify + экранирование </script> | JSON + кастомное экранирование |
| URL-параметры | Процентное кодирование (RFC 3986) | URL-кодировщик |
| JSON API-ответы | Нет (не HTML-контекст) | JSON-сериализатор |
Заключение
Кодирование HTML-сущностей необходимо при встраивании пользовательских данных в HTML для предотвращения XSS-атак. Ключ — понимание контекста: текстовое содержимое, атрибуты, JavaScript и URL имеют разные требования к кодированию.
Ключевые выводы:
- Всегда кодируйте пользовательский ввод при рендеринге HTML
- Кодируйте во время рендеринга, а не при сохранении
- Используйте кодирование по контексту: HTML-сущности для текста/атрибутов, JSON для script-тегов, процентное кодирование для URL
- Никогда не доверяйте клиентскому кодированию для серверной безопасности
- Тестируйте со специальными символами для проверки правильности кодирования
Для ручного тестирования и отладки используйте HTML-кодировщик/декодер, чтобы проверить, как кодируются символы и убедиться, что ваш вывод защищен от XSS-уязвимостей, сохраняя при этом легитимный контент.