DOMPurify: очистка HTML — 8 живых примеров и мастерская
Восемь политик очистки HTML: комментарии, статьи, письма, ссылки и SVG. Редактируйте исходник, сравнивайте результат, скачивайте HTML и JSON; примеры React, Vue и Node.js.

DOMPurify на практике: восемь живых политик очистки HTML, комментарии, статьи, письма, ссылки и SVG. Мастерская с редактором, предпросмотром, JSON-отчётом и исходниками для браузера и Node.js.
Что такое DOMPurify и какую проблему он решает
В личном кабинете появился редактор заметок, в CRM нужно показать письмо, а в базе знаний — описание со списками и таблицами. Во всех трёх случаях приложение получает HTML, который написал другой человек или сформировала внешняя система. Простая вставка такой строки в страницу передаёт браузеру не только текст, но и разметку с её возможностями. Поэтому между входными данными и показом нужен понятный контракт.
DOMPurify очищает HTML по заданным правилам: разбирает фрагмент через DOM, удаляет неподходящие элементы и атрибуты и возвращает результат для дальнейшего использования. Можно сохранить полезное оформление, одновременно ограничив доступный набор возможностей. Библиотека подходит для браузера, а с подходящей реализацией DOM — и для Node.js.
В этой публикации — восемь живых сценариев, редактор исходника, переключение политик, предпросмотр, список удалений и скачивание результата. Всё очищается настоящим DOMPurify 3.4.17 прямо в браузере. Исходники включают тот же модуль правил для Node.js, консольную обработку небольшого файла, тесты и инструкции для Windows, Linux и VPS.
Где применять: комментарии, редакторы, CRM и импорт контента
Самый понятный пример — комментарии с небольшим форматированием. Пользователю нужны абзацы, выделение, ссылка и список; возможность вставить произвольную форму, скрипт или стили ему обычно не требуется. Второй пример — редактор статьи: здесь нужны заголовки и таблицы, поэтому политика будет шире. Одна настройка для всех полей редко отражает реальные задачи интерфейса.
В CRM полезно показать текст входящего письма без внешних картинок и встроенных страниц. В службе поддержки — импортировать описание обращения. В каталоге — принять ограниченный фрагмент описания от поставщика. В корпоративной справке — оставить ссылки только на разрешённый домен. В редакторе значков — отдельно обработать SVG, не смешивая его возможности с обычным комментарием.
Начинайте с того, что человек должен увидеть и сделать. Составьте список нужных тегов, атрибутов и адресов, затем проверьте, что оформление сохранилось. Без этого очистка может оказаться формально работающей, но бесполезной: например, удалить таблицу с реквизитами или оставить ссылку, которую продукт не должен показывать.
Обычный текст, экранирование и очистка HTML — разные операции
Если поле содержит имя, статус, номер заказа или сообщение без форматирования, используйте обычный текстовый вывод. В браузере для этого подходит textContent. Символы угловых скобок останутся видимыми символами, а не превратятся в элемент страницы. Для такой задачи библиотека очистки часто вообще не нужна.
const label = document.querySelector('#customer-name');
label.textContent = userProvidedName;
Экранирование кодирует символы для конкретного места вставки. Очистка HTML разрешает часть разметки и удаляет неподходящие возможности. Это разные договорённости с автором: «показать написанное буквально» и «показать допустимое оформление». Выбирайте договорённость заранее, иначе пользователь будет видеть то теги, то неожиданно исчезнувшее форматирование.
Сохранение строки в базе само по себе не делает её пригодной для HTML. Важен последний шаг перед выводом. Также очищенный HTML не превращается в подходящее значение для JavaScript-кода, CSS, SQL или произвольного атрибута. О поведении обычного текстового вывода подробно рассказывает документация textContent.
Граница инструмента: что DOMPurify не проверяет
XSS, или межсайтовый скриптинг, возникает, когда недоверенные данные становятся выполняемым содержимым страницы. Очистка HTML помогает ограничить этот путь, но не устанавливает достоверность текста. Фрагмент может содержать ложное обещание, чужую персональную информацию или ссылку на нежелательный сайт с обычным HTTPS. Проверка прав, модерация, ограничения загрузок и правила самого продукта остаются задачами приложения. DOMPurify не заменяет эти решения.
Библиотека также не решает SQL-инъекции, проверку пароля, защиту серверных запросов к произвольным адресам или контроль доступа к файлам. Если после очистки вы извлекаете URL и загружаете его сервером, это уже отдельная операция с отдельными требованиями. Не переносите свойства HTML-очистки на другой канал использования строки.
Восемь политик этой мастерской демонстрируют конкретный набор решений. Они не являются универсальным фильтром для всех продуктов. Например, мы намеренно исключили пользовательские inline-стили, внешние изображения и произвольные data-атрибуты. Если вашему редактору нужны другие возможности, сначала определите область применения, затем измените политику и её поведенческие тесты.
Мини-мастерская: восемь сценариев с настоящей очисткой
Выберите сценарий и нажмите «Очистить HTML». Сравните исходник, готовую разметку и предпросмотр. Затем переключите политику, оставив тот же текст: сразу видно, почему комментарий и статья требуют разных правил. Можно изменить исходник вручную; до повторного запуска копирование и старые ссылки скачивания отключаются, чтобы результат нельзя было спутать с новой редакцией.
В каждой сцене указаны применение и ожидаемое поведение. Попробуйте сохранить таблицу в статье, убрать внешний пиксель из письма, разрешить только нужные схемы ссылок, очистить SVG-иконку и проверить домен справки. Список удалённых элементов помогает объяснить изменения, но сам по себе не доказывает безопасность результата.
Ввод обрабатывается локальным JavaScript без отправки содержимого на сервер мастерской. Скачать можно очищенный HTML-фрагмент и JSON-отчёт с версией библиотеки, выбранной политикой и диагностикой. Предпросмотр дополнительно изолирован; скачиваемый фрагмент не содержит его оболочку и ограничения. Для работы на телефоне есть отдельное открытие мастерской на весь экран.
Версии, установка и устройство проекта
В исходниках закреплены DOMPurify 3.4.17, JSDOM 31.0.0, Bootstrap 5.3.8 и сборщик esbuild 0.28.2. Для всего учебного проекта нужен Node.js 24.15 или новее: это требование нашего набора зависимостей, включая серверный DOM. Файл package-lock.json фиксирует разрешённые версии, а npm ci устанавливает именно этот набор.
npm ci --ignore-scripts
npm run build
npm test
Каталог src содержит читаемые сценарии и политики, tools — сборку, локальный сервер и CLI, test — проверки, public — готовую статическую мастерскую. В public находятся локально собранные скрипты и Bootstrap, поэтому ввод посетителя не отправляется стороннему CDN. Исходные лицензии зависимостей приложены отдельно.
Для нового проекта можно установить библиотеку обычным npm install dompurify, но скачанный пример воспроизводите через ci. Не смешивайте обновление зависимостей и проверку исходного архива: сначала получите его ожидаемый результат, затем обновляйте осознанно. Возможности и параметры библиотеки описаны в официальном репозитории DOMPurify.
Первое подключение: небольшой HTML-контракт
Простейший вызов принимает строку и возвращает очищенный фрагмент. В продукте полезнее сразу ограничить формат тем, что действительно нужно полю. В примере ниже остаются абзацы, переносы и два варианта выделения, а атрибуты не разрешены. Это подходит для короткого описания без ссылок и оформления страницы.
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em'],
ALLOWED_ATTR: [],
ALLOW_DATA_ATTR: false,
ALLOW_ARIA_ATTR: false,
RETURN_TRUSTED_TYPE: false
});
document.querySelector('#description').innerHTML = clean;
Вставляйте результат только в тот HTML-контекст, для которого его получили. Здесь это содержимое обычного контейнера страницы. Не собирайте из него строку обработчика события, CSS-правило или URL. Если приложение использует принудительные Trusted Types, потребуется другая форма присваивания — её разберём отдельно.
В мастерской общий модуль добавляет к каждой политике запрет style и srcdoc, отключает произвольные data- и ARIA-атрибуты и возвращает строку. Панель настроек показывает уже объединённую конфигурацию. При переносе копируйте весь договорённый набор правил, а не только красивую часть списка тегов.
Пример 1. Комментарий с полезным форматированием
Первый сценарий оставляет абзацы, выделение, список, цитату, код и ссылку. Это достаточно для обсуждения задачи, отзыва или сообщения поддержки. В исходнике есть также обработчик клика, изображение с обработчиком ошибки и скрипт. Они представлены текстом в редакторе; неочищенный фрагмент не вставляется в исполняемый предпросмотр.
const commentPolicy = {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol',
'li', 'a', 'code', 'blockquote'],
ALLOWED_ATTR: ['href', 'title'],
ALLOW_DATA_ATTR: false,
ALLOW_ARIA_ATTR: false,
FORBID_ATTR: ['style', 'srcdoc']
};
Проверьте практический результат: слова с выделением остаются, список читается, обработчик исчезает. Если пользователь вставил целый документ из редактора, большая часть его оформления может быть удалена. Интерфейс должен объяснять поддерживаемый формат до сохранения, иначе человек воспримет правильное ограничение как потерю своей работы.
Разрешённый href ещё не означает, что продукт одобряет любой адрес. Базовая очистка и дополнительная политика ссылок решают разные задачи. Для комментариев с ограниченными переходами возьмите отдельный сценарий links или docs и адаптируйте правила к своему ресурсу.
Пример 2. Статья с заголовками, кодом и таблицей
У статьи больше потребностей: подзаголовки, фрагменты кода, таблицы и цитаты. Во второй сцене эти элементы сохраняются, а форма и поля ввода удаляются. Разрешены colspan и rowspan для объединённых ячеек. Пользовательские стили по-прежнему исключены: визуальное оформление задаёт ваш сайт.
import { sanitizeHtml } from './src/policies.js';
const result = sanitizeHtml(DOMPurify, editorHtml, 'article');
articleContainer.innerHTML = result.html;
console.log(result.inputCharacters, result.outputCharacters);
Такой подход подходит базе знаний, справочнику и описанию услуги. Автор отвечает за структуру материала, а приложение — за ширину таблиц, размеры заголовков и цвета. Если редактор хранит собственную схему документа, полезно сначала получить из неё HTML, затем очистить фрагмент по правилам места показа.
Не требуйте побайтового совпадения с исходником: DOM-парсер нормализует разметку и может добавить недостающую структуру таблицы. Проверяйте сохранение смысла и допустимых элементов. После очистки не запускайте преобразователь, который снова добавит непроверенные атрибуты или вставит исходный HTML из другого поля.
Пример 3. Фрагмент письма без внешних изображений
Для CRM часто достаточно показать тему внутри текста, абзацы, таблицу и ссылку. В третьем сценарии внешняя картинка-пиксель и iframe не проходят политику. Это удобно для краткого просмотра обращения: сотрудник читает сообщение без загрузки изображений из него в нашей мастерской.
const result = sanitizeHtml(DOMPurify, messageFragment, 'email');
messagePreview.innerHTML = result.html;
Это обработчик фрагмента, а не полноценный почтовый клиент. Он не разбирает MIME, не проверяет вложения и не воспроизводит всю табличную вёрстку коммерческой рассылки. Inline-стили здесь удаляются, поэтому визуально письмо может отличаться от оригинала. Если нужна отдельная кнопка загрузки изображений, для неё потребуется собственный проверенный механизм.
Также важно разделить данные отправителя и HTML-тело. Адрес, имя, дата и статус должны выводиться обычным текстом и проверяться отдельно. Текст письма может убеждать перейти на чужой сайт даже после корректной очистки. Санитайзер отвечает за разметку, а продукт должен ясно показывать происхождение сообщения.
Пример 4. Ссылки: только HTTPS, mailto и tel
Иногда продукт допускает только определённые способы перехода. В политике links сохраняются HTTPS, mailto и tel; HTTP, относительные адреса и неподходящие схемы теряют href. Текст ссылки остаётся, поэтому пользователь не теряет само описание. Это дополнительное ограничение поверх обработки URL библиотекой.
const linkFilter = (node, data) => {
if (data.attrName !== 'href') return;
const value = data.attrValue.trim();
if (!/^(?:https:\/\/|mailto:|tel:)/i.test(value)) {
data.keepAttr = false;
}
};
Регулярное выражение здесь выбирает несколько бизнес-схем для уже разбираемого атрибута. Оно не заменяет HTML-парсер или основную очистку DOMPurify. Такой узкий фильтр отличается от попытки удалить весь опасный HTML одной регуляркой.
Проверка схемы не проверяет существование адреса, репутацию сайта, корректность телефона или принадлежность почтового ящика. Для действия «позвонить» может потребоваться отдельная нормализация номера. Для оплаты и входа в кабинет лучше использовать собственные явно заданные адреса, а не доверять любому HTTPS из пользовательского сообщения.
Пример 5. SVG обрабатываем отдельной политикой
SVG полезен для иконок и схем, но его возможности заметно шире обычного текстового комментария. Поэтому пятая сцена использует отдельный узкий список геометрии, размеров и цветов. В учебном значке остаются прямоугольник и контур галочки; обработчик события, скрипт, foreignObject и внешнее изображение удаляются. Полный allowlist находится в src/policies.js, а сокращённый вариант для геометрии выглядит так:
const svgPolicy = {
ALLOWED_TAGS: ['svg', 'g', 'path', 'rect', 'circle'],
ALLOWED_ATTR: ['viewBox', 'width', 'height', 'x', 'y',
'cx', 'cy', 'r', 'rx', 'd', 'fill', 'stroke', 'stroke-width'],
ALLOW_DATA_ATTR: false, ALLOW_ARIA_ATTR: false
};
const guardPaint = (node, data) => {
const value = data.attrValue.trim();
if (/[\\\u0000-\u001f\u007f]|url\s*\(/i.test(value)) {
data.keepAttr = false;
}
if (['fill', 'stroke'].includes(data.attrName) &&
!/^(?:[a-z]+|#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8}))$/i.test(value)) {
data.keepAttr = false;
}
};
DOMPurify.addHook('uponSanitizeAttribute', guardPaint);
try {
const clean = DOMPurify.sanitize(untrustedSvg, svgPolicy);
// clean можно передать в предусмотренное место вывода.
} finally {
DOMPurify.removeHook('uponSanitizeAttribute', guardPaint);
}
Одного удаления image и href недостаточно: внешняя ссылка может находиться в fill, stroke, filter или cursor. Поэтому проект запрещает фильтры, курсоры, маски, градиенты и другие функции со ссылками, а hook дополнительно отбрасывает url(…), обратную косую черту и управляющие символы в значениях. CSS escapes не должны обходить правило через альтернативную запись имени функции. Это ограничение действует в самом очищенном HTML, включая скачанный файл, независимо от CSP предпросмотра.
Для fill и stroke остаются именованные цвета, none/currentColor и hex-цвета; функции rgb()/hsl() и даже локальные ссылки url(#gradient) здесь намеренно исключены. Это учебная политика простых значков, а не универсальный конвертер любого SVG. Для богатых иллюстраций нужен отдельно разработанный и проверенный набор возможностей. Не переносите SVG-политику в поле комментария: раздельные правила легче объяснять, проверять и менять без расширения возможностей остальных полей.
Пример 6. Удалить теги — ещё не получить API обычного текста
Шестая сцена запрещает все теги и атрибуты. Она наглядно показывает, какое содержимое остаётся после очистки разметки. Но результат функции всё ещё является сериализованной HTML-строкой: специальные символы могут быть представлены сущностями. Это существенно при дальнейшей обработке.
const clean = DOMPurify.sanitize('<strong>A & B</strong>', {
ALLOWED_TAGS: [],
ALLOWED_ATTR: [],
RETURN_TRUSTED_TYPE: false
});
// clean is an HTML serialization, not an arbitrary plain-text converter.
plainTextLabel.textContent = originalPlainText;
Если передать такую сериализацию в textContent, сущность может стать видимым текстом, а не декодированным символом. Поэтому для имени, суммы или подписи кнопки лучше хранить и выводить обычную строку без промежуточной HTML-обработки. Не создавайте лишний цикл «кодировать — декодировать» ради задачи, которой не нужна разметка.
Для импорта текста из HTML заранее определите правила переноса строк, списков и таблиц. Удаление тегов не описывает эти правила полностью. Эта сцена помогает увидеть границу возможностей санитайзера и выбрать отдельное преобразование документа, если продукту нужен именно текстовый экспорт.
Пример 7. Id, name и защита от конфликтов DOM-свойств
Некоторым документам нужны именованные элементы. Но произвольные id и name могут конфликтовать с кодом страницы и её именованными свойствами. В седьмой политике включён SANITIZE_NAMED_PROPS: библиотека изолирует такие имена префиксом user-content-. Это отдельное правило для места, где атрибуты действительно нужны.
const namedPolicy = {
ALLOWED_TAGS: ['h2', 'p', 'a', 'strong'],
ALLOWED_ATTR: ['id', 'name', 'href'],
SANITIZE_NAMED_PROPS: true
};
Посмотрите на результат сцены: имя элемента меняется. Однако ссылка вида #settings не обязана автоматически превратиться в #user-content-settings. Поэтому простое включение флага может нарушить внутреннюю навигацию. Если она нужна, потребуется согласованное преобразование якорей и целевых идентификаторов с собственными тестами.
Часто проще вообще не разрешать id и name в пользовательском фрагменте. Код приложения должен получать элементы через явные селекторы и ссылки, а не предполагать, что имя из DOM безопасно стало глобальной переменной. Эта сцена показывает не декоративную настройку, а связь между разметкой и поведением окружающего интерфейса.
Пример 8. Справка со ссылками на один точный домен
Для внутренней инструкции может действовать правило: разрешены только ссылки на example.com по HTTPS. Восьмая сцена разбирает адрес через URL и сравнивает hostname целиком. Адрес example.com.evil.test не проходит, хотя содержит знакомую строку. Также исключены логин, пароль и нестандартный порт.
function isDocumentationLink(value) {
try {
const url = new URL(value);
return url.protocol === 'https:'
&& url.hostname === 'example.com'
&& !url.username && !url.password && !url.port;
} catch {
return false;
}
}
Стандартный HTTPS-порт 443 нормализуется URL-парсером, поэтому явное :443 не делает адрес запрещённым в этой политике. Относительные ссылки не проходят, поскольку здесь не задан базовый URL. Поддомены тоже не разрешены автоматически. Если они нужны, добавляйте понятное правило, а не поиск подстроки в полном адресе.
Такой список разрешений ограничивает направление ссылки, но не подтверждает содержание страницы. Домен example.com здесь учебный: замените его на свой и проверьте допустимые пути. Поведение hostname описано в документации URL. Не используйте этот пример как готовую проверку серверного скачивания адресов.
Hooks: добавляем правило и убираем его после вызова
Hooks позволяют расширить обработку атрибутов, но у экземпляра DOMPurify есть состояние. Если добавить фильтр домена и забыть его удалить, следующее поле может неожиданно получить чужие ограничения. В мастерской hook устанавливается только для нужной политики и снимается в finally — даже если во время обработки возникнет ошибка.
function cleanWithLinkRule(purify, input, config, linkFilter) {
purify.addHook('uponSanitizeAttribute', linkFilter);
try {
return purify.sanitize(input, config);
} finally {
purify.removeHook('uponSanitizeAttribute', linkFilter);
}
}
Тест проекта специально выполняет разные политики подряд и проверяет отсутствие такого переноса. При интеграции в большой сервис не разбрасывайте глобальные addHook и setConfig по несвязанным модулям. Держите правила рядом с функцией очистки и задавайте понятный жизненный цикл экземпляра.
Вызов sanitize синхронный, но приложение может иметь много компонентов и независимых задач. Удобнее экспортировать одну функцию с именем политики и проверкой входа, чем позволять каждому экрану менять общий экземпляр. Это также упрощает обновление: один модуль правил, один набор тестовых случаев, явные места использования.
Почему нельзя лечить весь HTML регулярным выражением
HTML — это не просто строки между угловыми скобками. Парсер восстанавливает ошибочную структуру, обрабатывает пространства имён и атрибуты, а браузер затем интерпретирует результат. Попытка удалить только слово script или несколько известных обработчиков не описывает эту модель. Новые варианты ввода быстро выходят за пределы такого списка.
DOMPurify работает с разобранным документом, а продукт задаёт разрешённые возможности. Именно поэтому наши тесты проверяют поведение настоящей библиотеки в DOM, а не совпадение с одной вручную составленной строкой. Узкое регулярное выражение для выбора HTTPS, mailto и tel дополняет этот процесс, но не подменяет его.
Не добавляйте после очистки цепочку самодельных замен, возвращающих удалённые атрибуты ради красивого отображения. Если обнаружилось полезное оформление, которое не сохранилось, измените политику осознанно и добавьте тест. Если нужен другой язык разметки, например Markdown, сначала определите порядок его преобразования в HTML и очистки конечного результата.
React: очищаем только там, где действительно нужен HTML
Обычное текстовое значение в JSX передаётся как текст. Специальный dangerouslySetInnerHTML нужен только для намеренной HTML-вставки. В таком месте можно использовать общий модуль политик, который уже применяет ограничения мастерской. Не передавайте туда исходный текст напрямую.
import DOMPurify from 'dompurify';
import { sanitizeHtml } from './policies.js';
export function CommentBody({ html }) {
const clean = sanitizeHtml(DOMPurify, html, 'comment').html;
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
// For ordinary text use: <div>{text}</div>
Этот фрагмент показывает клиентскую интеграцию с нашим модулем и его лимитом 20000 символов. Ошибку размера нужно обработать в интерфейсе формы до публикации. При серверном рендеринге потребуется согласованная серверная очистка и DOM-реализация; не считайте наличие браузерного import достаточным для любого SSR-окружения.
После вставки другой компонент не должен повторно подмешивать исходные атрибуты или необработанный контент. Поддерживайте одинаковую политику в редакторе, предпросмотре и опубликованном представлении. О назначении специального HTML-свойства рассказывает официальная документация React.
Vue: v-html получает результат, а не исходник
Во Vue обычная интерполяция подходит для текстовых значений. Для намеренного HTML есть v-html, и именно перед ним нужно определить правила допустимой разметки. Вычисляемое значение удобно связывает входное свойство с очищенным результатом; при изменении исходника отображение обновляется по тем же правилам.
<script setup>
import { computed } from 'vue';
import DOMPurify from 'dompurify';
import { sanitizeHtml } from './policies.js';
const props = defineProps({ html: { type: String, default: '' } });
const clean = computed(() =>
sanitizeHtml(DOMPurify, props.html, 'comment').html
);
</script>
<template><div v-html="clean"></div></template>
Не превращайте пользовательскую строку в Vue-шаблон и не компилируйте её как код компонента. Очистка HTML предназначена для содержимого, а не для разрешения автору выполнять шаблонный язык. Наличие параметра с похожим названием в библиотеке не заменяет эту архитектурную границу.
Как и в React, здесь отдельно решаются ошибки размера, серверный рендеринг и выбор политики для каждого поля. Подробные ограничения вокруг шаблонов и HTML-вставок описаны в руководстве безопасности Vue. Для простой подписи оставьте обычную интерполяцию и не вводите v-html без необходимости.
Node.js: тот же контракт с настоящим DOM через JSDOM
На сервере нет встроенного браузерного window. В исходниках его предоставляет JSDOM, а фабрика DOMPurify получает этот объект. Дальше вызывается тот же sanitizeHtml, поэтому список тегов и правила ссылок не расходятся между браузерной мастерской и CLI.
import { JSDOM } from 'jsdom';
import createDOMPurify from 'dompurify';
import { sanitizeHtml } from './src/policies.js';
const window = new JSDOM('').window;
try {
const purify = createDOMPurify(window);
const result = sanitizeHtml(purify, input, 'comment');
console.log(result.html);
} finally {
window.close();
}
Здесь не включается исполнение скриптов из входного документа. DOM-реализация участвует в обработке недоверенной разметки, поэтому обновлять нужно и DOMPurify, и JSDOM. Старый DOM-парсер может изменить реальные свойства всего решения, даже если сам санитайзер обновлён.
В учебном package.json JSDOM находится среди devDependencies, поскольку нужен для CLI и тестов. Если вы переносите этот код в постоянно работающий Node-сервис, включите нужные серверные библиотеки в его production-зависимости. Условия запуска и ограничения исполнения скриптов описаны в официальной документации JSDOM.
CLI: очистить небольшой файл и получить диагностический отчёт
Консольный инструмент удобен для импорта подготовленного фрагмента, проверки шаблона или воспроизводимого разбора проблемы. Он читает файл как UTF-8, применяет выбранную политику, записывает очищенный HTML и выводит JSON-диагностику. Команда одинакова в PowerShell и bash после установки зависимостей.
node tools/sanitize.mjs input.html clean.html comment
node tools/sanitize.mjs input.html clean-article.html article
Оба пути выбирает доверенный оператор. Существующий файл результата перезаписывается, поэтому используйте отдельное имя. Не превращайте пример в веб-API, принимающее от посетителя путь на сервере. Для загрузок понадобится отдельный поток приёма данных, контроль размера до чтения и ограниченная область хранения.
Модуль принимает до 20000 символов, но CLI читает файл до этой проверки. Это не защита процесса от произвольно большого файла. Для массовой обработки ограничивайте объём заранее. Отсутствующие аргументы дают код завершения 2, ошибка чтения, политики или записи — 1, успешная обработка — 0. JSON-вывод можно сохранить в журнал задания без публикации содержимого документа.
Trusted Types: тип результата и политика браузера
Trusted Types помогают контролировать опасные места HTML-вставки, но не выбирают корректный алгоритм очистки сами. В приложении нужно определить политику, которая создаёт TrustedHTML из проверенного результата. Это отдельный вариант интеграции; в мастерской он не включён, её JSON и скачивание работают со строками.
// Integration example for a browser with the Trusted Types API.
const htmlPolicy = window.trustedTypes.createPolicy('aen-html', {
createHTML: input => DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['p', 'strong', 'em'],
ALLOWED_ATTR: [],
ALLOW_DATA_ATTR: false,
ALLOW_ARIA_ATTR: false,
RETURN_TRUSTED_TYPE: false,
TRUSTED_TYPES_POLICY: null
})
});
container.innerHTML = htmlPolicy.createHTML(dirty);
Внутри createHTML нужен обычный строковый результат, поэтому указан RETURN_TRUSTED_TYPE: false. В версии примера TRUSTED_TYPES_POLICY: null также запрещает библиотеке создавать собственную внутреннюю fallback-политику dompurify. Не передавайте htmlPolicy обратно в DOMPurify: её createHTML уже вызывает санитайзер, и получится круговая зависимость.
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types aen-html
Это фрагмент заголовка для согласования с общей CSP, а не готовая политика всех ресурсов сайта. Проверьте поддержку API целевыми браузерами. Параметр RETURN_TRUSTED_TYPE: true решает другую задачу — просит сам DOMPurify вернуть TrustedHTML при доступности API. Связь политик и контролируемых мест вставки описана в документации Trusted Types.
Как устроен предпросмотр: sandbox и дополнительная CSP
Неочищенный ввод находится только в textarea. Очищенный код показывается через textContent, а визуальная версия помещается в iframe с пустым sandbox: без разрешения скриптов и без allow-same-origin. Внутри документа предпросмотра есть собственная ограничивающая CSP. Она дополняет очистку, но не заменяет её.
default-src 'none';
style-src 'unsafe-inline';
img-src data:;
base-uri 'none';
form-action 'none'
Разрешение inline-стиля нужно для нашей фиксированной оболочки предпросмотра; пользовательский style удаляется политикой. Перед показом только в preview дополнительно удаляется href у ссылок, чтобы переходы не отвлекали от опыта. Скачиваемый HTML и JSON сохраняют первоначальный очищенный результат с разрешёнными ссылками.
Поэтому перенесённый на другой сайт фрагмент не получает sandbox и CSP автоматически. Место использования должно иметь собственные правила. Не копируйте разрешения предпросмотра как универсальную CSP всего приложения: набор ресурсов и сценариев у него другой. Возможности изоляции описаны в документации iframe и Content-Security-Policy.
Windows: собрать, проверить и открыть мастерскую
Установите Node.js версии 24.15 или новее, распакуйте исходники и откройте PowerShell в каталоге проекта. Использование npm.cmd позволяет явно вызвать Windows-исполняемый файл npm. Сначала восстановите зависимости из lock, затем выполните сборку и проверки.
Set-Location .\dompurify-project
node --version
npm.cmd ci --ignore-scripts
npm.cmd run build
npm.cmd test
npm.cmd start
Мастерская откроется по адресу http://127.0.0.1:8172/. Сервер слушает только локальный интерфейс; остановка — Ctrl+C в том же терминале. Если порт занят другой программой, задайте свободный через переменную PORT перед запуском. Не останавливайте чужой сервис ради учебного примера.
$env:PORT = '8180'
npm.cmd start
Не открывайте index.html двойным щелчком через file://: ES-модули и загрузка локальных ресурсов рассчитаны на HTTP. Если после изменения src результат не обновился, повторите build и обновите страницу. Сборка публикует готовые файлы в public, а редактировать удобнее исходные модули.
Linux и VPS: исходники, статика и проверка заголовков
На Linux порядок тот же: нужен подходящий Node.js и npm, а не случайно установленная старая версия из репозитория. Команды выполняются обычным пользователем в каталоге проекта. Учебному серверу не нужны root-права или доступ к системным каталогам.
cd dompurify-project
node --version
npm ci --ignore-scripts
npm run build
npm test
npm start
Для публичной мастерской достаточно содержимого public. Соберите его локально или в CI и разместите в отдельном каталоге статики. Постоянный Node-процесс и systemd здесь не требуются. Например, Caddy может отдавать готовую страницу и её локальные ресурсы:
demo.example.com {
root * /srv/dompurify-workshop/public
encode zstd gzip
file_server
}
Настройте DNS, HTTPS и обычные права чтения для веб-сервера. Не публикуйте node_modules, рабочие документы и секреты. В Nginx подключите стандартный mime.types: JavaScript-модуль должен отдаваться с корректным Content-Type. После загрузки проверьте реальный адрес assets/app.js, а затем откройте саму мастерскую на узком экране.
curl -I https://demo.example.com/assets/app.js
Проверки, обновления и ограничения доказательств
В проекте восемнадцать поведенческих тестов на настоящих DOMPurify и JSDOM. Они проходят по восьми сценам, проверяют сохранение полезной структуры, ограничения ссылок, изоляцию именованных свойств, отсутствие переноса hooks, границы ввода и повторную очистку конкретного комментария. Отдельно проверены отключение ссылок только в preview и восстановление интерфейса после ошибки размера. Это проверка нашего контракта на выбранных данных.
npm test
npm outdated
npm audit
Набор не является полным аудитом всех возможных XSS и не доказывает универсальную идемпотентность любой будущей политики. После изменения тегов, DOM-реализации или места вставки добавляйте свои случаи. Нужны и разрешённые примеры: фильтр, который удаляет всё полезное, может пройти только отрицательные проверки, но сломать интерфейс.
Обновления санитайзера и DOM-парсера имеют практическое значение. Делайте их в отдельной ветке, сохраняйте lock, повторяйте тесты и просмотр реальных документов. npm audit сообщает об известных проблемах зависимостей; отсутствие сообщений не заменяет проверку архитектуры. В рабочем сервисе также контролируйте лимиты, ошибки обработки и неожиданное изменение объёма удаляемого оформления.
Типичные ошибки интеграции и понятный UX редактора
Частая ошибка — очистить предпросмотр, а при сохранении или публикации снова использовать исходную строку. Другая — очистить HTML, затем передать его плагину, который добавляет непроверенные атрибуты. Третья — разрешить богатую политику статьи во всех полях, включая короткий комментарий. Во всех случаях проблема возникает в цепочке обработки, а не только в одном вызове sanitize.
Для удобного редактора покажите поддерживаемый формат, дайте сравнить результат и объясните удаление неподдерживаемых элементов человеческим языком. Не называйте сообщение «ничего не удалено» гарантией безопасности. DOMPurify.removed полезен для диагностики конкретного вызова, а не для разрешения публикации или обхода следующей проверки.
Не записывайте весь исходник в общедоступные логи: в письмах и заметках могут быть персональные данные. Для статистики обычно достаточно политики, версии, длины входа и кода результата. Если храните оригинал для повторной обработки после обновления правил, ограничьте доступ и никогда не выводите его как готовый HTML. Пользователь должен понимать, какое представление действительно будет опубликовано.
Исходники и официальные материалы для продолжения
В архиве есть читаемые политики, восемь исходных сцен, HTML5-мастерская, сборка, Node.js CLI и тесты. Собственный код проекта распространяется под MIT; условия зависимостей приложены отдельно. Начать удобнее с одного поля своего приложения: определить нужное оформление, выбрать политику и проверить реальные документы.
После первого внедрения проследите весь путь строки: от редактора или импорта через хранение до конечного места HTML-вставки. Зафиксируйте допустимые теги, ссылки, размер и поведение при ошибке. Именно такой понятный контракт позволяет использовать DOMPurify как полезную часть приложения и объяснить его результат человеку.


