Skip to content

fix(markdown): sanitize content_html server-side (F-009) - #8

Merged
LuanTrindade95 merged 4 commits into
mainfrom
fix/server-side-html-sanitization
Sep 20, 2026
Merged

LuanTrindade95 merged 4 commits into
mainfrom
fix/server-side-html-sanitization

Conversation

@LuanTrindade95

@LuanTrindade95 LuanTrindade95 commented Sep 20, 2026 •

Copy link
Copy Markdown
Owner

Fecha o achado F-009 do audit de 2026-06-13: content_html, derivado de Markdown de terceiros, era armazenado e devolvido pela API sem sanitização no servidor. O Angular era a única barreira, então qualquer outro consumidor — export, integração, client alternativo — recebia HTML executável.

Causa raiz

Dois vetores independentes:

  1. GithubFlavoredMarkdownConverter era instanciado sem configuração, mantendo os padrões html_input=allow e links inseguros permitidos — HTML bruto embutido no Markdown passava intacto.
  2. DocumentResource e DocumentData liam content_html direto do model, sem segunda barreira, então documentos ingeridos antes da correção seguiam expostos.

Correção

Sanitização em duas camadas, sem dependência nova (ver ADR-28):

  • Ingestão: conversão passa a usar html_input=escape e allow_unsafe_links=false, e a saída é sanitizada antes de persistir.
  • Leitura: accessor Document::contentHtml() sanitiza a cada acesso, sem reescrever a coluna — é o que cobre os documentos antigos sem migração nem reprocessamento de dados.

App\Services\Markdown\HtmlSanitizer é um allowlist de tags e atributos sobre DOMDocument. href/src aceitam apenas http, https, mailto e caminhos relativos, com remoção de bytes de controle antes da checagem de esquema. Isso neutraliza a classe de bypass do CVE-2026-71478 (commonmark 2.8.2) independentemente da atualização daquele pacote.

A sanitização do Angular (MarkdownRendererService) não foi tocada e permanece como defesa em profundidade.

Testes

  • tests/Unit/Services/Markdown/HtmlSanitizerTest.php: 19 casos maliciosos (script, handlers on*, iframe, svg onload, object/embed, style, form, javascript:, data:text/html, entidades codificadas, variação de caixa, bytes de controle) e 8 casos legítimos.
  • tests/Feature/MarkdownSanitizationTest.php: pipeline completo de parse, e documento com content_html malicioso gravado direto no banco servido sanitizado pela API.

Auditoria adversarial

Executada em banco isolado, descartado ao final; o banco de desenvolvimento não recebeu migrate nem reset.

  • 19 payloads ingeridos e lidos pela API: 0 tags executáveis, handlers on* ou esquemas perigosos sobrevivendo.
  • Segunda rota (/plugins/{slug}/versions/{version}/documents, 38 documentos) verificada contra a falha "sanitiza só numa rota": 0 falhas.
  • Linhas inseridas por query builder: coluna crua permanece maliciosa, resposta da API sai sanitizada.
  • Renderização legítima: 18/18 checagens de estrutura presentes, bloco de código escapado e não como tag.

Gates

Após o merge da main (que trouxe o PR #7), com a imagem do backend reconstruída — a Dockerfile faz COPY . . e o container não tem bind mount, então rodar os testes sem rebuild valida código velho:

  • Pest: 72 passed (329 assertions), incluindo os 17 testes que vieram da main.
  • Pint: PASS, 125 arquivos.
  • Sem skip/only/todo em backend/tests ou frontend/src.
  • composer audit (advisories pré-existentes) e as 2 falhas de Jest em runtime-config.spec.ts são anteriores a esta branch e estão registradas no backlog.

Passo de deploy

DiscoveryCache guarda payloads resolvidos por 300s, então entradas cacheadas antes do deploy seguem servindo HTML não sanitizado até expirar. Invalidar o cache ao subir — comando em docs/HUMAN-ACTIONS.md.

🤖 Generated with Claude Code

LuanTrindade95 and others added 4 commits September 20, 2026 00:32
…009)

GithubFlavoredMarkdownConverter passed raw HTML and javascript:/data: links
through untouched, and content_html was returned by the API exactly as
stored. Any consumer other than the sanitizing Angular viewer was exposed
to stored XSS, including rows already persisted before this fix.

- Harden the converter config (html_input=escape, allow_unsafe_links=false)
  as a first layer at parse time.
- Add HtmlSanitizer, a dependency-free DOM-based allowlist filter for the
  tags/attributes Markdown legitimately produces (headings, code, tables,
  GFM task lists, links, images), and apply it to freshly parsed content.
- Add a content_html accessor on Document that runs the same sanitizer on
  every read, so API responses are safe even for documents whose HTML was
  written to the database before this fix (no data migration performed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…SS payloads

Unit-tests HtmlSanitizer directly against the required payload matrix
(script, on* handlers, iframe, svg onload, object/embed, style, form,
javascript:/data:text/html links, encoded entities, case variation,
control-byte scheme bypass) plus the legitimate rendering set.

Feature-tests the full MarkdownParser pipeline against the same malicious
Markdown/HTML mix, and proves a document whose content_html was written
directly to the database (bypassing ingestion entirely) is served
sanitized through GET /api/v1/documents/{document}.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mark F-009 as mitigated, add ADR-26 for the two-layer server-side HTML
sanitization, update canonical state and backlog, document the discovery
cache invalidation deploy step, and leave a handoff with the audit evidence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sanitization

Renumbers the sanitization ADR to ADR-28 because main took ADR-26 and
ADR-27, keeps both remediation phases in the canonical state, and drops
the two backlog items completed by this branch and by PR #7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@LuanTrindade95
LuanTrindade95 merged commit 29a7881 into main Sep 20, 2026
4 checks passed
@LuanTrindade95
LuanTrindade95 deleted the fix/server-side-html-sanitization branch September 23, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant