# Markdown é a fonte, HTML é derivado: sanitização com uma borda só

> Sanitizar na escrita ou na leitura? A resposta que evita XSS é ter exatamente um lugar onde o HTML nasce confiável — e nunca sanitizar de novo depois.

Publicado em 2026-07-28 por Time Orion · Blog da Orion
Versão canônica: https://orion.moe/blog/sanitizacao-com-uma-borda-so
Índice do blog: https://orion.moe/blog

---

Todo sistema que aceita texto formatado de um humano e o exibe como HTML enfrenta a mesma decisão: **onde sanitizar?**

Existem duas escolas. Sanitizar na **leitura** (guarda o original, limpa toda vez que exibe) e sanitizar na **escrita** (limpa uma vez, guarda o resultado limpo).

Escolhemos a segunda, e a decisão vem com uma regra que é mais importante que a escolha em si.

## Como funciona no nosso blog

O autor escreve **Markdown**. No momento de salvar, o servidor converte para HTML e passa por uma allowlist estrita. As duas versões ficam no banco: o Markdown, que é a fonte editável, e o HTML, que é o derivado servido.

O site público consome o HTML e injeta direto na página. **Sem sanitizar de novo.**

Essa última frase costuma causar desconforto, então vale explicá-la.

## Por que não sanitizar de novo

Parece que sanitizar duas vezes é mais seguro. Não é — é mais frágil, por dois motivos.

**Primeiro: duas allowlists divergem.** No dia em que alguém adiciona uma tag na origem e esquece do consumidor, o conteúdo legítimo quebra silenciosamente. No dia em que o consumidor é mais permissivo que a origem, a segunda passada não acrescenta nada. Uma das duas está sempre errada, e ninguém sabe qual.

**Segundo, e mais importante: duas bordas significam nenhuma dona.** Quando a responsabilidade é compartilhada, cada lado presume que o outro cuidou. É assim que se cria um terceiro consumidor — um app, um feed, uma API — que esquece de sanitizar porque "isso já vem limpo".

A regra que adotamos é explícita e está escrita no código:

> **O HTML que sai do save é confiável. A borda é uma só, e é aqui.**

Se algum dia um consumidor novo aparecer, ele herda a garantia — em vez de herdar a obrigação.

## A allowlist é o produto

Sanitizar não é rodar uma biblioteca: é **decidir o que o seu conteúdo pode conter**. A nossa é curta de propósito:

- Estrutura: títulos de segundo nível para baixo, parágrafos, listas, citação, tabela, código.
- Ênfase: negrito, itálico, tachado.
- Link e imagem.

E as decisões que valem mais que a lista:

**Sem `h1`.** O título do post já é o H1 da página. Um segundo H1 no corpo é erro de semântica e de SEO.

**Só `https` e `mailto`.** `javascript:` óbvio que não; `http:` também não, porque link inseguro numa página nossa é aviso de conteúdo misto de graça.

**Imagem só com URL absoluta `https`.** Aqui mora uma sutileza que quase passou: a opção de esquemas permitidos da biblioteca filtra URLs **que têm esquema**. Um caminho relativo não tem — e passava. Foi preciso um filtro explícito para descartar imagem cujo `src` não começa com `https://`.

**Link externo ganha `rel` de segurança.** Aplicado na transformação, não confiado ao autor.

## O que a escolha custa

Seria desonesto não dizer.

**Reprocessar exige script.** Se a allowlist mudar — ou se a conversão melhorar —, o HTML já gravado continua velho. É preciso um comando que rode o Markdown de novo. Guardar a fonte é o que torna isso possível; é por isso que as duas colunas existem.

**A prévia tem que usar o mesmo caminho.** Se a tela de prévia renderizar o Markdown de um jeito diferente do save, ela mente. No nosso editor, a prévia chama a **mesma função** do save — nunca uma renderização client-side "aproximada".

## O wrapper que não deve ser reusado

Uma armadilha sutil: já existia no sistema uma sanitização de HTML — a do webmail, que exibe e-mail recebido. Reusá-la teria sido natural.

Teria sido errado. Aquele wrapper é **permissivo de propósito**: e-mail recebido precisa de fidelidade visual, com estilos e atributos que jamais deveriam entrar num post nosso. São dois problemas com a mesma aparência e requisitos opostos.

Sanitização não é uma função utilitária universal. Ela é específica do contexto — e o contexto é "o que este conteúdo, vindo desta origem, tem o direito de fazer nesta página".

## O resumo

Escolha uma borda. Coloque-a onde o conteúdo nasce. Escreva no código que ela é a única, para que o próximo consumidor não invente a segunda. E desconfie de reusar a allowlist de outro contexto — a semelhança é aparente.

O mesmo princípio de fronteira única aparece no [gate de aprovação da Aura](/blog/gate-de-aprovacao-da-aura): um caminho, auditado, para a coisa perigosa virar registro.
