DevToys Pro

бесплатные веб-инструменты для разработчиков

Блог
Оцените нас:
Попробуйте расширение для браузера:
← Назад к блогу

HTML-сущности: когда экранировать, а когда нет

13 мин чтения

Кодирование HTML-сущностей — одна из тех тем, которые кажутся простыми, пока вам не нужно решить, кодировать ли пользовательский ввод, попадающий в HTML-атрибут, текстовое содержимое, URL-параметр или JSON-данные, встроенные в тег <script>. Ошибка открывает дверь для атак межсайтового скриптинга (XSS). Слишком агрессивное кодирование ломает легитимный контент.

Это руководство объясняет, когда кодирование HTML-сущностей необходимо для безопасности, когда оно необязательно и как безопасно обрабатывать данные в разных HTML-контекстах, не нарушая функциональность и не создавая уязвимостей.

Что такое HTML-сущности?

HTML-сущности — это специальные последовательности, представляющие зарезервированные символы в HTML. Они начинаются с & и заканчиваются ;.

СимволИменованная сущностьЧисловая сущностьЗачем кодировать
<&lt;&#60;Начинает HTML-тег
>&gt;&#62;Заканчивает HTML-тег
&&amp;&#38;Начинает сущность
"&quot;&#34;Разделитель атрибута
'&apos;&#39;Разделитель атрибута

Браузер автоматически декодирует эти сущности при рендеринге страницы. Если вы видите &lt;script&gt; в HTML-коде, браузер отображает <script> как обычный текст, а не выполняет его как тег.

Контекст имеет значение: где нужно кодирование

Золотое правило: требования к кодированию зависят от контекста. HTML имеет несколько контекстов, где могут появляться данные, и у каждого свои правила экранирования.

Контекст 1: Текстовое содержимое HTML

Текстовое содержимое между тегами (не внутри атрибутов) требует кодирования для <, > и &:

<p>Комментарий пользователя: <script>alert('XSS')</script></p>

<!-- Уязвимо: браузер выполняет скрипт -->

<p>Комментарий пользователя: &lt;script&gt;alert('XSS')&lt;/script&gt;</p>

<!-- Безопасно: браузер отображает как текст -->

Обязательное кодирование: <&lt;, >&gt;, &&amp;

Необязательно: Кавычки (" и ') не требуют кодирования в текстовом содержимом, потому что они не имеют особого значения вне атрибутов.

Контекст 2: HTML-атрибуты

Атрибуты требуют кодирования для " (или ', если используются одинарные кавычки):

<input value="Ввод пользователя: " onclick="alert(1)">

<!-- Уязвимо: атакующий внедряет атрибут onclick -->

<input value="Ввод пользователя: &quot; onclick=&quot;alert(1)">

<!-- Безопасно: кавычки закодированы, нет внедрения атрибута -->

Обязательное кодирование в атрибутах с двойными кавычками: "&quot;, &&amp;

Обязательное кодирование в атрибутах с одинарными кавычками: '&apos; или &#39;, & &amp;

Контекст 3: JavaScript внутри HTML

Встраивание данных в теги <script> — самый опасный контекст. Кодирование HTML-сущностей здесь не помогает, потому что браузер парсит содержимое скрипта как JavaScript, а не как HTML.

<script>
  const userName = "&lt;script&gt;alert(1)&lt;/script&gt;";
</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="&quot;&gt;&lt;script&gt;alert(1)&lt;/script&gt;">

// Безопасно: отображается как обычный текст

Атака 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="&quot; onmouseover=&quot;alert(1)">Наведи на меня</div>

// Безопасно: кавычки — буквальный текст, не разделители атрибутов

Когда кодирование необязательно

Статический контент

Если содержимое жестко закодировано и не контролируется пользователем, кодирование необязательно (хотя все равно хорошая практика):

<p>Тег <code> используется для кода.</p>

<!-- Работает нормально, нет пользовательского ввода -->

<p>Тег &lt;code&gt; используется для кода.</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) });

// Результат: база данных содержит "&lt;Алиса&gt;" вместо "<Алиса>"
// Если позже отправить это в 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-уязвимостей, сохраняя при этом легитимный контент.