Insights / Технические материалы

Безопасный рендеринг Markdown-ссылок в ответах AI-чата

Техническая заметка о том, как безопасно превращать Markdown-ссылки из ответов AI-чата в HTML. Парсинг с допуском пробелов, trim для href, allowlist, DOM-рендеринг, fallback и тесты рассматриваются отдельно.

  • Технологии
  • Веб-сайт
  • AI
  • Безопасность
  • Astro
Безопасный рендеринг Markdown-ссылок в ответах AI-чата
Содержание
  1. Ответы AI не являются доверенным HTML
  2. Проблема не только в пробелах
  3. Делайте trim перед проверкой href
  4. Allowlist зависит от продукта
  5. Fallback в текст
  6. Тестируйте плохие случаи
  7. Не реализуйте весь Markdown по умолчанию
  8. Итог

Если AI-чат отвечает Подробнее см. [Services]( /services/ ), ссылка может не отрендериться, а исходный Markdown останется на экране.

Acecore столкнулся с этим в контактном AI-чате и поправил renderer в PR с исправлением Markdown-ссылок.

Эта статья использует небольшое исправление как вход в тему безопасного превращения AI-ответов в DOM.

Ответы AI не являются доверенным HTML

Вывод модели нужно считать текстом.

В чате полезны ссылки, жирный текст и списки. Но innerHTML заставляет браузер интерпретировать любую строку, которую произвела модель.

Не нужно реализовывать весь Markdown. Нужен небольшой renderer, который распознает только поддерживаемые элементы и создает безопасные DOM-узлы.

Проблема не только в пробелах

Конкретная ошибка была в ссылке:

[Services](/services/)

Строгая regex часто считает, что URL не содержит пробелов:

/\[([^\]]+)\]\(([^)\s]+)\)/;

[^)\s]+ отклоняет пробелы, поэтому ( /services/ ) не распознается. Исправление допускает пробелы внутри скобок, а затем нормализует значение.

/\[([^\]]+)\]\(\s*([^)]+?)\s*\)/;

Но ослабить parser недостаточно. Нормализованное значение обязательно нужно проверить.

Делайте trim перед проверкой href

Порядок должен быть таким:

  1. Извлечь label и raw href из Markdown
  2. Применить trim() к raw href
  3. Проверить href по allowlist
  4. Создать <a> только если href разрешен
const href = String(rawHref || "").trim();

if (label && isSafeMarkdownHref(href)) {
  const link = document.createElement("a");
  link.href = href;
  link.rel = "noopener noreferrer";

  if (/^https?:\/\//i.test(href)) {
    link.target = "_blank";
  }

  link.textContent = label;
  parent.appendChild(link);
}

Проверяемое значение должно быть тем же, что попадает в DOM.

Allowlist зависит от продукта

Каждый сайт должен решить, какие URL может показывать AI.

Тип Пример Решение
Внутренний путь /services/ Разрешить
Тот же origin https://acecore.net/... Разрешить
Официальный LINE https://lin.ee/... Разрешить как официальный канал
mailto mailto:info@acecore.net Только фиксированный адрес
tel tel:05088902788 Только фиксированный номер
Другие внешние Любой URL По умолчанию не ссылать
function isSafeMarkdownHref(href) {
  if (href.startsWith("/")) return true;

  try {
    const url = new URL(href, window.location.origin);
    if (url.origin === window.location.origin) return true;
    if (url.hostname === "acecore.net") return true;
    if (url.hostname === "lin.ee") return true;
  } catch {
    return false;
  }

  return href === "mailto:info@acecore.net" || href === "tel:05088902788";
}

Рекрутинговый сайт может разрешать job boards, SaaS может разрешать документацию и status page. Функция должна отражать политику продукта.

Fallback в текст

Если ссылка не проходит проверку, удаление не всегда лучший вариант.

В контактном AI-чате текстовый fallback сохраняет контекст для пользователя и помогает разработчикам увидеть, что модель пыталась вывести.

Renderer должен не только создавать безопасные ссылки, но и безопасно отказывать.

Тестируйте плохие случаи

Минимальный набор:

Ввод Ожидаемый результат
[Services](/services/) Внутренняя ссылка
[Services]( /services/ ) Внутренняя ссылка после trim
[LINE]( https://lin.ee/example ) Разрешенная внешняя ссылка
[Bad](javascript:alert(1)) Не превращается в ссылку
[External](https://example.com/) Не ссылка, если домен не разрешен
[Broken](/services/ Отображается как текст

В PR #99 было проверено, что варианты с пробелами и без них ведут к ожидаемому URL.

Не реализуйте весь Markdown по умолчанию

Для чата обычно достаточно:

  • Абзацы
  • Списки
  • Жирный текст
  • Inline-code
  • Ссылки

Таблицы, изображения, raw HTML и footnotes быстро расширяют ответственность renderer. Даже с библиотекой политика HTML и URL остается отдельным решением.

Итог

Рендеринг Markdown-ссылок в AI-ответах выглядит как небольшая UI-правка, но на деле задает границу доверия к выводу модели.

Практическое правило: сначала текст, маленькое подмножество, trim перед проверкой, строгий allowlist и безопасный fallback.

Поток рендеринга ссылок

  1. Text

    Сначала рассматривать ответ модели как обычный текст.

  2. Parse

    Находить только те Markdown-элементы, которые реально поддерживает чат.

  3. Validate

    Делать trim для href и разрешать только внутренние URL или одобренные домены.

  4. Render

    Создавать безопасные элементы через DOM API, не через innerHTML.

Какие решения нужно разделять

Слабый рендеринг

  • Вставлять ответы AI прямо в innerHTML
  • Пытаться сразу реализовать весь Markdown
  • Не распознавать ссылки с пробелами вокруг URL
  • Обрабатывать внешние URL и javascript: одинаково

Малый и безопасный рендеринг

  • Принимать ответы как текст и превращать в DOM только нужное
  • Поддерживать только подмножество Markdown для чата
  • Проверять URL после trim
  • Оставлять запрещенные URL обычным текстом

Чеклист внедрения

  • Не доверять ответам AI как HTML
  • Разрешать пробелы вокруг URL в Markdown-ссылках
  • Всегда делать trim href перед проверкой
  • Разрешать только внутренние пути, текущий origin и нужные внешние домены
  • Явно задавать target и rel для внешних ссылок
  • Сохранять запрещенные ссылки как текст
  • Тестировать опасные URL и сломанный Markdown

Частые вопросы

Достаточно ли markdown-it или marked?

Даже с библиотекой нужно отдельно решить, как обрабатывать HTML, какие цели ссылок разрешены, как добавлять target и rel и как отклонять опасные URL. Для чата небольшого собственного renderer может быть достаточно.

Опасно ли разрешать пробелы вокруг URL?

Риск не в пробелах, а в том, что разрешается после trim. Проверка нормализованного href сохраняет allowlist строгим.

Нужно ли удалять запрещенные URL?

Обычно текстовый fallback проще для отладки и сохраняет контекст. Более строгая политика может удалить всю ссылку.