O que é sanitização de HTML e por que o Reddit mudou

Recentemente, o Reddit anunciou que passa a bloquear HTML simples como inseguro em determinados contextos. A decisão gerou debate intenso na comunidade de devs: afinal, o que exatamente e considerado inseguro num simples <p>texto aqui</p>?

A resposta esta no modelo de segurança do Content Security Policy (CSP) e na forma como navegadores modernos interpretam HTML inserido dinamicamente. Mesmo tags aparentemente inofensivas podem ser vetores de XSS (Cross-Site Scripting) dependendo do contexto em que são renderizadas e dos atributos que carregam.

Para devs que constroem aplicações web, esse debate e um lembrete prático: sanitização de HTML não e paranoia, e uma necessidade técnica real. Entender por que é como sanitizar corretamente faz a diferença entre uma aplicação segura e uma vulnerável.

Como ataques XSS funcionam em HTML aparentemente seguro

XSS (Cross-Site Scripting) acontece quando código malicioso e injetado numa página e executado no contexto do usuário. O vetor clássico e o seguinte: um usuário envia um texto que é armazenado e depois renderizado para outros usuários. Se o app não sanitiza, o texto pode conter tags HTML com JavaScript.

O problema e que o ataque evoluiu muito além de <script>alert(1)</script>. Tags como <img src='x' onerror='código malicioso'>, <a href='JavaScript:...'>, <svg onload='...'> e dezenas de outros vetores permitem executar JavaScript sem uma tag script visível. Atributos de evento (onmouseover, onfocus, onblur) em praticamente qualquer tag HTML podem ser aproveitados.

A situação ficou ainda mais complexa com o Markdown e editores rich text: muitos parsers de Markdown geram HTML a partir da entrada do usuário, e se esse HTML não for sanitizado antes de ser inserido no DOM, o XSS e possível mesmo em plataformas que parecem não aceitar HTML diretamente.

🔴
Cuidado

Nunca insira HTML de origem externa diretamente no DOM usando innerHTML ou dangerouslySetInnerHTML sem sanitizar antes. Mesmo que você filtre tags script, ha dezenas de outras formas de executar JavaScript via atributos HTML.

Principais técnicas de sanitização de HTML

Existem três abordagens principais, em ordem de segurança crescente:

  • Allowlist de tags e atributos: apenas as tags e atributos explicitamente listados como seguros são mantidos. Tudo mais e removido. E a abordagem mais segura e recomendada pela OWASP.
  • Blocklist: remove tags e atributos considerados perigosos. Raramente suficiente porque atacantes encontram vetores não listados na blocklist.
  • Encoding: converte todos os caracteres especiais HTML (< > & etc.) em entidades HTML. Seguro quando você quer exibir o HTML como texto literal, mas não quando precisa renderizar o HTML.

Para aplicações que precisam renderizar HTML gerado pelo usuário (editores rich text, comentários formatados, posts com markdown), a allowlist e a única abordagem confiável. Definir quais tags são permitidas (p, br, strong, em, a, ul, ol, li, h2, h3, blockquote, code, pre) e quais atributos são aceitáveis (href em a, com validação do protocolo) elimina a superfície de ataque.

💡
Dica

Para atributo href, sempre valide que o protocolo e http:// ou https://. Links com JavaScript:, data: ou vbscript: são vetores de XSS mesmo sem tags script.

Como implementar sanitização na prática

Em JavaScript no frontend, a biblioteca mais usada e confiável e a DOMPurify. Ela usa o próprio parser do navegador para processar o HTML e aplicar a allowlist, o que a torna muito mais confiável que regex:

# Instalação
npm install dompurify

# Uso básico (JavaScript)
import DOMPurify from 'dompurify';

// Sanitizar HTML antes de inserir no DOM
const htmlLimpo = DOMPurify.sanitize(htmlDoUsuario);
elemento.innerHTML = htmlLimpo;

// Com configuração customizada (allowlist estrita)
const htmlLimpo2 = DOMPurify.sanitize(htmlDoUsuario, {
  ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li'],
  ALLOWED_ATTR: ['href'],
  ALLOW_DATA_ATTR: false
});

No backend Python, a biblioteca bleach (Mozilla) era o padrão por anos, mas foi descontinuada. A alternativa atual mais recomendada e a nh3 (bindings Python para a biblioteca Ammonia em Rust):

# Instalação
pip install nh3

# Uso básico Python
import nh3

# Sanitizar com allowlist padrão conservadora
html_limpo = nh3.clean(html_do_usuario)

# Com tags personalizadas
html_limpo2 = nh3.clean(
    html_do_usuario,
    tags={'p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li', 'code', 'pre'},
    attributes={'a': {'href'}}
)
⚠️
Atenção

Nunca tente escrever sua própria sanitização com regex. Expressões regulares não conseguem parsear HTML corretamente e sempre tem casos de borda que um atacante pode explorar. Use bibliotecas testadas e auditadas.

Exemplo prático: comentários em uma aplicação web

Um caso de uso clássico: um sistema de comentários onde usuários podem formatar seu texto. O fluxo seguro e:

// Frontend (React) - sanitizando antes de renderizar
import DOMPurify from 'dompurify';

function Comentário({ htmlConteudo }) {
  const sanitized = DOMPurify.sanitize(htmlConteudo, {
    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'code', 'blockquote'],
    ALLOWED_ATTR: []
  });
  return 
; } // Backend (Python) - sanitizando ao salvar no banco import nh3 def salvar_comentario(usuario_id: int, html_raw: str): html_seguro = nh3.clean( html_raw, tags={'p', 'br', 'strong', 'em', 'code'}, attributes={} ) db.comentários.insert({'usuario_id': usuario_id, 'conteúdo': html_seguro})

Sanitizar nos dois lugares (frontend e backend) e a prática mais segura: o backend protege o banco de dados e outros clientes que possam consumir a API, e o frontend garante que mesmo dados já no banco não causem problemas ao serem renderizados.

🚀
Pro tip

Configure um Content Security Policy (CSP) rigoroso no seu servidor além de sanitizar HTML. CSP e uma segunda linha de defesa: mesmo que um XSS passe pela sanitização, o navegador bloqueia a execução do script se a política não permitir scripts inline.

Comparação: DOMPurify vs nh3 vs outras opcoes

  • DOMPurify (JS/browser): o padrão para frontend. Usa o parser nativo do navegador, muito confiável, atualizado frequentemente. Não funciona no Node.js sem um ambiente DOM (use jsdom).
  • nh3 (Python): bindings para Ammonia (Rust), muito rápido, mantido ativamente após o bleach ser descontinuado. Escolha atual para backend Python.
  • sanitize-html (Node.js): popular no ecossistema Node, configurável, funciona no servidor sem DOM.
  • Bleach (Python, descontinuado): ainda funciona mas não recebe mais atualizações de segurança. Migrar para nh3 e o caminho recomendado.

Pontos positivos e limitações

Sanitização de HTML resolve o problema de XSS via conteúdo usuário de forma sistemática. Uma vez implementada corretamente com uma allowlist bem definida, protege contra toda uma classe de ataques sem necessidade de análise caso a caso.

As limitações existem: sanitização tem custo de processamento (geralmente insignificante, mas mensurável em volumes muito altos), pode remover formatação valida que os usuários esperam ver (se a allowlist for muito restrita), e não protege contra outros tipos de ataque como SQL injection ou SSRF que requerem outras contramedidas.

Também é importante entender que sanitização protege contra XSS mas não e um substituto para autenticação, autorização ou validação de dados de negócio. Cada camada de segurança tem seu papel específico.

Casos de uso reais

CMS e plataformas de blog: editores rich text como TinyMCE, Quill e ProseMirror geram HTML. Sanitizar o output antes de salvar no banco e obrigatório mesmo em plataformas voltadas apenas para administradores, por principio de defesa em profundidade.

Sistemas de comentários: qualquer plataforma com comentários de usuários precisa sanitizar. Isso inclui desde comentários em e-commerce até issues no GitHub (que tem sua própria sanitização).

Emails transacionais com conteúdo do usuário: se você inclui conteúdo gerado pelo usuário em emails HTML (nome, mensagem, etc.), sanitize antes de montar o template. XSS em email pode ser usado para roubar credenciais via phishing.

Integração com Markdown: todo parser de Markdown que gera HTML precisa ter a saída sanitizada. Mesmo que o Markdown básico seja seguro, muitos parsers suportam HTML literal embutido no Markdown.

Dicas e boas práticas

💡
Dica

Mantenha um inventario de todos os lugares no seu código onde HTML externo e inserido no DOM ou salvo no banco. Uma busca por innerHTML, dangerouslySetInnerHTML e raw HTML templates e um bom ponto de partida para uma auditoria rápida.

⚠️
Atenção

URLs em atributos href precisam de validação de protocolo além da sanitização de HTML. Links com JavaScript: são removidos pelo DOMPurify por padrão, mas verifique que sua configuração personalizada mantem essa proteção.

🚀
Pro tip

Adicione testes automatizados com payloads de XSS conhecidos (OWASP XSS Filter Evasion Cheat Sheet) nos pontos de entrada do seu sistema. Um teste que tenta inserir um payload e verifica que o JavaScript não foi executado e melhor que qualquer revisão manual.

🔴
Cuidado

Atualize regularmente as bibliotecas de sanitização. Novos vetores de XSS são descobertos constantemente, e as bibliotecas são atualizadas para proteger contra eles. Uma versão antiga do DOMPurify pode ter vulnerabilidades conhecidas.

Vale a pena se preocupar com sanitização?

Sim, sempre. XSS continua sendo uma das vulnerabilidades mais comuns em aplicações web segundo o OWASP Top 10. A boa noticia e que a solução e simples quando aplicada sistematicamente: use uma biblioteca de sanitização confiável, defina uma allowlist conservadora e sanitize toda entrada externa antes de renderizar.

O debate do Reddit sobre HTML simples sendo inseguro e um lembrete de que mesmo plataformas maduras e bem financiadas continuam revisando e endurecendo sua postura de segurança em relação ao conteúdo do usuário. Para uma aplicação menor, estabelecer esse hábito desde o inicio e muito mais barato que corrigir uma violação depois.

O próximo passo e buscar por innerHTML e dangerouslySetInnerHTML no seu projeto atual e verificar se ha sanitização em cada ponto. Se não houver, adicionar DOMPurify leva menos de 5 minutos.