Кейс · Бэкенд · TypeScript

md-safe: Markdown-парсер и санитайзер

API и веб-демо безопасного рендера Markdown в HTML на TypeScript. Парсер подмножества CommonMark написан с нуля, результат всегда проходит санитайзер по allowlist (тоже свой): готовых markdown-it, marked и dompurify в проекте нет. Проверка: корпус из 242 XSS-векторов и независимый проверяющий вывода, который не использует код санитайзера.

  • TypeScript
  • Node.js 24
  • Markdown
  • XSS
  • allowlist
  • fast-check
  • vitest
  • axe
  • Docker
  • GitHub Actions

01 · Как выглядит

Скриншоты

Скриншот проекта «md-safe: Markdown-парсер и санитайзер» на экране 1280 пикселей
Десктоп, 1280 px
Скриншот проекта «md-safe: Markdown-парсер и санитайзер» на экране телефона, 390 пикселей
Телефон, 390 px

02 · О проекте

Что это и зачем

Задача: безопасно показывать пользовательский Markdown, не доверяя готовым библиотекам вслепую, и уметь доказать, что XSS не проходит.

Решение: TypeScript, парсер подмножества CommonMark и санитайзер по allowlist написаны с нуля (около 380 строк санитайзера). Проверка независима от реализации: корпус из 242 векторов и отдельный проверяющий вывода со строгим шаблоном.

Ограничения

Allowlist и тесты не заменяют аудит.

Корпус не выполнялся в реальном браузере в CI: проверяется структура вывода независимым шаблоном.

Лимиты частоты живут в памяти процесса.

Проект работает на VPS; тот же парсер работает в живом приложении scratch-notes.

Что внутри

  • 242 XSS-вектора из публичных списков (OWASP XSS Filter Evasion, PortSwigger, HTML5 Security Cheatsheet): скрипты, обработчики событий, обфускация схем, data:, mXSS через svg, math, noscript, style
  • Каждый вектор проходит как HTML, как Markdown с санитизацией и с экранированием, с allow_data_images и без
  • Независимый проверяющий tests/safe.ts не использует код санитайзера: любой лишний тег, атрибут, схема или сырой «<» это нарушение
  • fast-check: на случайных строках вывод проходит проверяющего, санитайзер идемпотентен, обычный текст не меняется
  • 829 тестов; тяжёлые входы (60 тысяч «[», 40 тысяч «*a», 20 тысяч вложенных «>», 100 тысяч «<b>») быстрее 3 секунд: разбор линейный
  • Результат: 200 запросов p50 9 мс, p95 31 мс, около 27 МиБ RAM

03 · Как запустить

Запуск одной командой

git clone <репозиторий по запросу> md-safe
cd md-safe
npm ci && npm test
npm run build && npm start   # http://localhost:8120

04 · Код

Репозиторий приватный

Исходники лежат в приватном репозитории. Напишите в Telegram, открою доступ.

Запросить код