惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hacker News: Ask HN
Hacker News: Ask HN
H
Help Net Security
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
Jina AI
Jina AI
Stack Overflow Blog
Stack Overflow Blog
量子位
博客园_首页
Vercel News
Vercel News
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Forbes - Security
Forbes - Security
IT之家
IT之家
N
News and Events Feed by Topic
S
Security Affairs
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Webroot Blog
Webroot Blog
Recorded Future
Recorded Future
L
LangChain Blog
Y
Y Combinator Blog
AI
AI
MyScale Blog
MyScale Blog
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Know Your Adversary
Know Your Adversary
AWS News Blog
AWS News Blog
Help Net Security
Help Net Security
Cyberwarzone
Cyberwarzone
L
Lohrmann on Cybersecurity
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Google Online Security Blog
Google Online Security Blog
V2EX - 技术
V2EX - 技术
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
PCI Perspectives
PCI Perspectives
I
Intezer
T
Tenable Blog
G
Google Developers Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Troy Hunt's Blog
L
LINUX DO - 最新话题
云风的 BLOG
云风的 BLOG
C
CXSECURITY Database RSS Feed - CXSecurity.com
有赞技术团队
有赞技术团队
O
OpenAI News
P
Proofpoint News Feed
TaoSecurity Blog
TaoSecurity Blog
C
Check Point Blog
Last Week in AI
Last Week in AI
S
Schneier on Security
Simon Willison's Weblog
Simon Willison's Weblog
Blog — PlanetScale
Blog — PlanetScale

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
HTML Sanitizer API: конец эпохи DOMPurify и XSS-страданий
Igor Agapov · 2026-06-24 · via Все публикации подряд на Хабре

Простой

9 мин

114

Инженеры узнают о межсайтовом скриптинге (Cross-Site Scripting, XSS) тремя способами.

Счастливчики узнают о нем благодаря полезному анализу кода или проактивному правилу проверки кода. Внимательные — во время аудита безопасности, который выявляет уязвимость до того, как она попадет в продакшн.

А есть те, кто узнает о нем через страдания, когда их сайт становится уязвимым. Когда злоумышленник внедряет скрипт, который крадет токены сессий из localStorage, перехватывает файлы cookie или перенаправляет пользователей на фишинговый сайт. Я лично присоединился к клубу «пострадавших» еще в 2005 году, когда встроенная Flash-подпись на форуме, которым я владел, превратилась в кошмар с точки зрения безопасности… но это уже другая история.

В этой статье мы рассмотрим, как браузер, наконец, снимает с нас бремя очистки данных (sanitizing) благодаря новому HTML API Sanitizer.

❯ Проблема innerHTML

Обозначим проблему. На заре интернета innerHTML был волшебной палочкой, позволяющей превращать строки в элементы DOM:

const container = document.getElementById('content');
const userInput = '<img src="x" onerror="alert(\'XSS\')">';
container.innerHTML = userInput;

При выполнении этого кода браузер пытается загрузить несуществующее изображение, терпит неудачу и запускает скрипт в onerror. Поздравляем, вы только что подверглись XSS-атаке.

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

  • пользовательский контент — комментарии, отзывы или любые другие формы пользовательского ввода, отображаемые на странице. Обычно эти данные хранятся в базе данных и обрабатываются позже. Если приложение не обезвреживает пользовательский ввод, это может привести к хранимому (stored) XSS

  • параметры URL — злоумышленники могут создавать URL-адреса с вредоносными данными в параметрах запроса. Если приложение отображает эти параметры на странице без надлежащей проверки, это может привести к отраженному (reflected) XSS. Например, страница поиска, которая принимает параметр запроса и отображает его на странице без проверки, может быть использована для атаки

Исторически это решалось с помощью DOMPurify. Это де-факто библиотека для очистки HTML в JavaScript. Она работает путем анализа входной строки, удаления любых опасных элементов или атрибутов и возврата безопасной версии HTML:

import DOMPurify from 'dompurify';

const container = document.getElementById('content');
const userInput = '<img src="x" onerror="alert(\'XSS\')">';
const sanitizedInput = DOMPurify.sanitize(userInput);

container.innerHTML = sanitizedInput;

Или, если вы используете React, можно сделать что-то подобное с помощью dangerouslySetInnerHTML для отображения очищенного контента:

import DOMPurify from 'dompurify';

function Comment({ content }) {
  const sanitizedContent = DOMPurify.sanitize(content);
  return <div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />;
}

DOMPurify — это фантастический инструмент, отлично справляющийся с очисткой данных, но не без недостатков. Он поставляется в минифицированном виде размером около 23,3 КБ (примерно 8,71 КБ в сжатом виде), требует поддержки и, по сути, повторяет парсинг HTML, выполняемый браузером.

Последний пункт имеет решающее значение. Библиотеки типа DOMPurify всегда были довольно хрупким решением. API парсинга, доступные веб-браузеру, не всегда точно соответствуют тому, как браузер фактически отображает строку в виде HTML в «реальном» DOM. Хуже того, этим библиотекам приходится постоянно следить за развитием браузеров, поскольку то, что когда-то было безопасным, может превратиться в бомбу замедленного действия в момент появления новой функции платформы. Это заставляет разработчиков библиотек играть в догонялки с каждым релизом браузера, и как только библиотека достигает размера и богатства DOMPurify, эта гонка превращается в работу на полный рабочий день. Браузер, с другой стороны, точно знает, когда и как он будет выполнять код. Внедрение обезвреживания в браузер означает, что он по определению остается синхронизированным с парсером.

❯ Новый HTML Sanitizer API

Веб-платформа теперь включает новые API, которые делают парсинг и обезвреживание HTML более безопасным. Спецификация определяет безопасные способы вставки HTML в DOM как альтернативу старому innerHTML.

API предоставляет 6 методов, разделенных на 2 группы:

  • безопасные методы: Element.setHTML(), ShadowRoot.setHTML(), Document.parseHTML(). Эти методы всегда удаляют опасный с точки зрения XSS контент, независимо от переданных настроек

  • небезопасные методы: Element.setHTMLUnsafe(), ShadowRoot.setHTMLUnsafe(), Document.parseHTMLUnsafe(). Эти методы допускают любой контент, определенный настройками

Рассмотрим эти методы подробнее.

> setHTML: безопасный способ вставки HTML

Метод setHTML — дополнение DOM API, позволяющее разработчикам устанавливать контент HTML способом, защищенным от уязвимостей XSS. При использовании setHTML браузер автоматически обезвреживает ввод, удаляя любые потенциально опасные элементы или атрибуты. Он безопасен по умолчанию. Мы можем настраивать его, но любой опасный контент все равно будет удален перед рендерингом. Он, например, всегда будет удалять элементы <script> и атрибуты on*. Он эффективно перезаписывает наши слишком «мягкие» настройки.

Простейшее использование не нуждается в настройке. Просто вызываем setHTML со строкой:

const maliciousInput = '<img src="x" onerror="alert(\'XSS\')">';
document.getElementById('content').setHTML(maliciousInput);

// Результат: <img src="x"> — атрибут onerror удален, что предотвращает атаку XSS.

Вот и все. Скрипт в атрибуте onerror удален, поскольку браузер выполнил логику обезвреживания в процессе парсинга, используя встроенные дефолтные безопасные настройки. Если вам интересно, какие элементы и атрибуты допускает дефолтный конфиг, на MDN есть полный список.

❯ Настраиваемое обезвреживание

Спецификация позволяет определять объект конфигурации с указанием разрешенных или запрещенных элементов и атрибутов. Определение правильной конфигурации может быть немного сложным, поскольку можно случайно указать элемент как в списке разрешенных, так и в списке запрещенных, или указать атрибут несколько раз. API строг на этот счет: при передаче невалидной конфигурации выбрасывается исключение TypeError. Это обеспечивает осведомленность разработчиков о любых противоречиях или избыточности в их конфигурации.

Рассмотрим пример конфигурации с разрешенным списком:

const config = {
  elements: ["em", "strong", "b", "i", "ul", "li"],
  attributes: ["id"],
  replaceWithChildrenElements: ["span", "div"],
};
const customSanitizer = new Sanitizer(config);

Эта конфигурация разрешает только определенный набор элементов и атрибутов. Все, что не входит в список, удаляется. Настройка replaceWithChildrenElements позволяет определять элементы, которые должны заменяться потомками вместо полного удаления. Поэтому, например, <div> во вводе будет удален, а его содержимое останется.

Конфигурация с запрещенным списком:

const config = {
  removeElements: ["span", "script"],
  removeAttributes: ["lang", "id", "class", "style"],
  comments: false,
};
const customSanitizer = new Sanitizer(config);

Эта конфигурация определяет элементы и атрибуты, которые должны быть удалены из ввода. Настройка comments определяет, должны ли сохраняться комментарии HTML. В данном случае они удаляются.

Нельзя определять elements и removeElements в одной конфигурации, поскольку они служат разным целям. Тоже самое касается attributes и removeAttributes. В противном случае, выбрасывается исключение TypeError. Можно комбинировать elements и removeAttributes или attributes и removeElements.

Обратите внимание, что в обоих примерах мы не заботились об опасных атрибутах, таких как инлайновые обработчики событий (on*). Это объясняется тем, что setHTML безопасен по умолчанию. Он удаляет все, что может привести к уязвимостям XSS, независимо от конфигурации.

setHTMLUnsafe: запасной вариант

setHTMLUnsafe — небезопасный аналог setHTML:

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

  • в случае setHTMLUnsafe наш конфиг — единственный источник ограничений. Если мы разрешим onclick, он останется. Отсутствие конфига означает отсутствие обезвреживания

Существует 2 основных причины использования этого метода:

  1. Декларативные теневые корневые элементы (shadow roots). setHTML удаляет их по умолчанию, поэтому для их сохранения в настоящее время нужно использовать setHTMLUnsafe.

  2. Намеренное разрешение определенных «небезопасных» атрибутов. Иногда нам необходимы инлайновые обработчики событий или что-то подобное.

Разница в коде:

const input = "<img src=x onclick=alert('onclick') onerror=alert('onerror')>";

// setHTMLUnsafe с кастомным конфигом: onclick разрешен, onerror удаляется
// (поскольку мы не указали его в списке, а не потому что API обеспечивает безопасность по умолчанию).
const lessSafeConfig = new Sanitizer({
  attributes: ["onclick"],
});
document.getElementById('output').setHTMLUnsafe(input, { sanitizer: lessSafeConfig });

onerror удаляется, потому что не указан в разрешенном списке, а не потому что setHTMLUnsafe обеспечивает безопасность по умолчанию. Если мы напишем attributes: ["onclick", "onerror"], оба атрибута останутся. В случае setHTML это неважно. Оба атрибута будут удалены.

parseHTML и parseHTMLUnsafe: обезвреживание без вставки

Иногда мы не хотим рендерить HTML сразу. Возможно, мы сначала хотим его распарсить, исследовать, преобразовать и только потом решить, что с ним делать. Для этого предназначены Document.parseHTML() и Document.parseHTMLUnsafe().

const untrustedHTML = '<p>Hello <script>alert("xss")</script>world</p>';

// Возвращает обезвреженный Document, который можно исследовать, обходить и из которого можно извлекать элементы
const doc = Document.parseHTML(untrustedHTML);
console.log(doc.body.innerHTML); // <p>Hello world</p>

parseHTML похож на setHTML в том, что уязвимости XSS всегда удаляются. parseHTMLUnsafe не выполняет обезвреживание по умолчанию.

Это может быть полезным для таких вещей, как однократное создание обезвреженного DocumentFragment и его многократное повторное использование, или выполнение проверок обезвреженного вывода перед решением о его рендеринге.

❯ Случаи реального использования

Давайте проясним одну важную вещь: даже с новым API серверного обезвреживания никто не отменял. Клиентское обезвреживание предназначено для улучшения пользовательского опыта и немедленной безопасности. Злоумышленник может обойти клиентский код и обратиться к API напрямую. Аналогией может служить валидация ввода пользователя на клиенте для лучшего UX и его валидация на сервере для бизнес-логики и безопасности.

В 704-м эпизоде ​​подкаста ShopTalkShow Дэйв Руперт и Крис Койер пригласили Фредерика Брауна из Mozilla для обсуждения HTML Sanitizer API. Они говорили об использовании этого API для оптимистичного обновления UI — популярного шаблона во фронтенд-разработке.

В случае комментариев, например, когда пользователь нажимает кнопку «Опубликовать», мы обычно полагаемся на бэкэнд, который обрабатывает комментарий и возвращает его клиенту, который его рендерит. Это занимает время и создает неоптимальный UX. Доверять необработанному пользовательскому вводу и отображать его немедленно может быть рискованно, но с новым API мы можем безопасно отображать комментарий сразу, не дожидаясь его обработки бэкэндом. В результате получается гораздо более удобный UI без ущерба для безопасности.

import React, { useState, useRef, useEffect } from 'react';

type Comment = { id: number; content: string };

const sanitizerConfig = { elements: ["b", "i", "em", "ul", "li"] };
const sanitizer = new Sanitizer(sanitizerConfig);

function CommentItem({ html }: { html: string }) {
  const ref = useRef<HTMLLIElement>(null);

  useEffect(() => {
    if (!ref.current) return;
    if ('setHTML' in ref.current) {
      ref.current.setHTML(html, { sanitizer });
    } else {
      // Резерв для браузеров, которые пока не поддерживают этот API: используем DOMPurify
      // или рендерим индикатор загрузки до получения обезвреженного ответа от сервера.
      ref.current.textContent = html;
    }
  }, [html]);

  return <li ref={ref} />;
}

const CommentSection = () => {
  const [comments, setComments] = useState<Comment[]>([]);

  const handleSubmit = (userInput: string) => {
    // 1. Оптимистичное обновление - рендерим сразу, безопасно
    const newComment = { id: Date.now(), content: userInput };
    setComments((prev) => [...prev, newComment]);

    // 2. Отправляем на сервер (который по-прежнему является источником истины)
    postComment(userInput);
  };

  return (
    <ul>
      {comments.map((comment) => (
        <CommentItem key={comment.id} html={comment.content} />
      ))}
    </ul>
  );
};

Отметим несколько вещей:

  • Sanitizer создается один раз на уровне модуля, а не внутри компонента. Его создание при каждом рендеринге является пустой тратой ресурсов

  • вызов setHTML в useEffect обернут в предохранитель, чтобы компонент не сломался в браузерах, которые пока не поддерживают этот API

  • мы работаем с <li> через ref, вместо того, чтобы смешивать dangerouslySetInnerHTML с механизмом согласования (diffing) React. Такое сочетание обычно вызывает проблемы с гидратацией

Вот где setHTML блистает во всей красе. Мы не можем передавать необезвреженную строку в dangerouslySetInnerHTML без риска создания уязвимости XSS, а API предоставляет нам эргономичную и безопасную альтернативу.

Еще несколько случаев, когда этот API может быть очень полезным:

  • Редакторы WYSIWYG. Пользователи обычно пишут контент в текстовых редакторах и вставляют его, перетаскивая огромный, некорректный HTML и встроенные стили. Отслеживание события paste и его обработка с помощью setHTML позволяют очистить контент перед рендерингом

  • Предварительный просмотр Markdown в реальном времени. Даже если поле ввода никогда не покидает браузер, все равно необходимо проверять сгенерированный HTML перед его отображением

  • Внешние каналы. RSS-ленты, синдицированный контент, встроенные фрагменты кода и все, что поступает извне, должно быть очищено перед тем, как попасть в DOM

❯ Заключение

HTML Sanitizer API — это значительный шаг вперед в повышении безопасности и эффективности веб-разработки. Он переводит безопасность из разряда «задачи библиотеки» в разряд «примитивов платформы». Благодаря этому мы получаем более высокую производительность, меньший размер пакетов и более безопасное поведение по умолчанию.

На момент написания статьи (май 2026 г.) поддержка этого API браузерами находится на ранней стадии. Firefox 148 поддерживает его с февраля 2026 года. Chrome поддерживает его в Canary за флагом, а Safari еще не начал работу над реализацией, хотя команда заявила о своем положительном отношении к этой фиче. Этот API еще не является базовым, а это значит, что для его использования в продакшне сегодня по-прежнему требуется резервный вариант (DOMPurify).

К счастью, веб всегда позволял нам использовать функции до их полной стандартизации или поддержки. Используйте этот API как прогрессивное улучшение уже сейчас и следите за поддержкой. День, когда он получит полную поддержку, станет днем, когда многие пакеты станут немного меньше, а многие приложения — немного безопаснее.


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале