Безопасный рендеринг Markdown-ссылок в ответах AI-чата
Содержание
Если 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
Порядок должен быть таким:
- Извлечь label и raw href из Markdown
- Применить
trim()к raw href - Проверить href по allowlist
- Создать
<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:[email protected] | Только фиксированный адрес |
| 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:[email protected]' || 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.
Поток рендеринга ссылок
Text
Сначала рассматривать ответ модели как обычный текст.
Parse
Находить только те Markdown-элементы, которые реально поддерживает чат.
Validate
Делать trim для href и разрешать только внутренние URL или одобренные домены.
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?
Опасно ли разрешать пробелы вокруг URL?
Нужно ли удалять запрещенные URL?
Комментарии
Gui
Генеральный директор Acecore. Руководит бизнес-системами, вебом, базами данных и инфраструктурой, качеством и внедрением ИИ от формулирования бизнес-задач до проектирования, запуска и дальнейшего улучшения. Опирается на практическую экспертизу C#/.NET и также учитывает PHP/JavaScript, SQL Server/PostgreSQL/MySQL и Linux/Windows Server, проектируя требования, технологический выбор, стандарты качества и GitHub-ориентированные процессы разработки как единую систему. Встраивает генеративный ИИ в процессы разработки, проверки и организации информации как практическую основу, помогающую небольшим командам быстрее и надежнее достигать результата.
Хотите узнать больше о наших услугах?
Мы обеспечиваем комплексную поддержку: разработка систем, веб-дизайн, графический дизайн и IT-образование.
Похожие статьи
Как развивать сайт на Astro + Cloudflare по функциям 7 июня 2026 г. в 19:00
Как добавить комментарии в Astro-блог только на Cloudflare 7 июня 2026 г. в 18:00
Руководство по внедрению Sveltia CMS 7 июня 2026 г. в 16:00