Белые списки тегов и атрибутов

При работе с HTML-контентом, особенно когда он поступает из внешних источников, крайне важно контролировать, какие теги и атрибуты допустимы. Это предотвращает внедрение небезопасного кода и обеспечивает корректное отображение контента. В экосистеме JavaScript для работы с Markdown и HTML, библиотеки Remark и Rehype предоставляют мощные средства для реализации белых списков тегов и атрибутов.

Основные понятия

Белый список тегов (allowed tags) — это перечень HTML-тегов, которые разрешено оставлять в контенте. Все остальные теги автоматически удаляются или экранируются. Например, разрешить <p>, <a>, <strong>, <em>, и запретить <script>, <iframe>.

Белый список атрибутов (allowed attributes) — это набор разрешённых атрибутов для конкретных тегов. Даже разрешённый тег может быть опасным, если его атрибуты содержат вредоносный код. Например, у <a> разрешён атрибут href, но запрещён onclick.

Настройка белых списков с Rehype

Для фильтрации HTML чаще всего используют rehype-sanitize. Он позволяет задавать конфигурацию, которая определяет допустимые теги и атрибуты.

Пример базовой конфигурации:

import { unified } from 'unified';
import rehypeParse from 'rehype-parse';
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
import rehypeStringify from 'rehype-stringify';

const schema = {
  ...defaultSchema,
  tagNames: ['p', 'a', 'strong', 'em', 'ul', 'li'],
  attributes: {
    a: ['href', 'title'],
    p: [],
    li: [],
    strong: [],
    em: []
  }
};

const processor = unified()
  .use(rehypeParse, { fragment: true })
  .use(rehypeSanitize, schema)
  .use(rehypeStringify);

const html = '<p oncl ick="alert(1)">Текст <a href="https://example.com">ссылка</a></p>';
const sanitized = processor.processSync(html).toString();
console.log(sanitized);

В этом примере:

  • <p> сохраняется, но атрибут onclick удаляется.
  • <a> сохраняется только с разрешённым атрибутом href.
  • Любые непредусмотренные теги полностью удаляются.

Расширение схемы для специфических тегов

Иногда необходимо разрешить нестандартные или кастомные теги. Для этого достаточно добавить их в tagNames и задать допустимые атрибуты:

schema.tagNames.push('custom-widget');
schema.attributes['custom-widget'] = ['data-id', 'class'];

Теперь <custom-widget data-id="123" class="highlight"></custom-widget> пройдёт через фильтр без изменений.

Интеграция с Remark

Remark используется для парсинга Markdown, который затем преобразуется в HTML через Rehype. Для работы с белыми списками применяют remark-rehype с последующей обработкой через rehype-sanitize:

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeSanitize from 'rehype-sanitize';
import rehypeStringify from 'rehype-stringify';

const markdownProcessor = unified()
  .use(remarkParse)
  .use(remarkRehype)
  .use(rehypeSanitize, schema)
  .use(rehypeStringify);

const md = 'Текст с [ссылкой](https://example.com) и <script>alert(1)</script>';
const htmlOutput = markdownProcessor.processSync(md).toString();
console.log(htmlOutput);

Здесь происходит последовательное:

  1. Преобразование Markdown в AST с помощью remark-parse.
  2. Преобразование AST Markdown в AST HTML через remark-rehype.
  3. Очистка HTML по белым спискам тегов и атрибутов через rehype-sanitize.
  4. Генерация финального HTML через rehype-stringify.

Практические рекомендации

  • Белые списки лучше определять как константы и централизованно использовать во всём проекте.
  • Разрешать только те теги и атрибуты, которые реально нужны для отображения контента.
  • Для ссылок <a> и изображений <img> проверять, чтобы атрибуты href и src не содержали потенциально опасные схемы (jav * ascript: и пр.).
  • При расширении схемы создавать тестовые Markdown и HTML-файлы, чтобы убедиться, что фильтр работает корректно.

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