Sanitização de saída: contexto importa
1. Por que a sanitização de saída é diferente da validação de entrada?
Muitos desenvolvedores cometem um erro crítico: acreditam que validar dados na entrada é suficiente para garantir segurança em todas as saídas. Essa confusão entre validação e sanitização é uma das principais causas de vulnerabilidades em aplicações web.
Objetivos distintos:
- Validação de entrada: rejeita dados maliciosos ou fora do formato esperado. Exemplo: um campo de CPF que aceita apenas 11 dígitos.
- Sanitização de saída: transforma dados para exibição segura em um contexto específico. Exemplo: converter
<script>em<script>ao exibir em HTML.
O erro comum: confiar que dados validados na entrada são seguros para qualquer saída. Um nome de usuário pode ser perfeitamente válido em um banco de dados, mas conter caracteres que quebram uma página HTML ou injetam código JavaScript.
Consequências reais:
- XSS (Cross-Site Scripting): dados exibidos sem sanitização permitem execução de scripts maliciosos no navegador da vítima.
- Injeção de SQL em logs: logs que registram consultas SQL com dados não sanitizados podem ser manipulados para executar comandos no banco.
- Quebra de formatação: dados com caracteres especiais quebram JSON, XML ou CSV gerados dinamicamente.
2. O contexto de saída define a técnica de sanitização
Cada contexto de saída possui regras únicas de escape. Usar a mesma função para todos é receita para desastre.
HTML:
// Dados recebidos: <script>alert('XSS')</script>
// Saída segura em HTML: <script>alert('XSS')</script>
// Caracteres a escapar:
// < → <
// > → >
// & → &
// " → "
// ' → '
JavaScript:
// Dados recebidos: Olá "mundo" \n novo
// Saída segura em JS: "Olá \"mundo\" \\n novo"
// Escape necessário:
// " → \"
// ' → \'
// \ → \\
// \n → \\n
// \t → \\t
URL:
// Dados recebidos: caminho com espaços & símbolos
// Saída segura em URL: caminho%20com%20espa%C3%A7os%20%26%20s%C3%ADmbolos
// Caracteres que devem ser percent-encoded:
// espaço → %20
// & → %26
// < → %3C
// > → %3E
CSS:
// Dados recebidos: url(javascript:alert(1))
// Saída segura em CSS: url%28javascript%3Aalert%281%29%29
// Propriedades dinâmicas sempre devem escapar valores
// para evitar injeção de estilos maliciosos
3. Sanitização para contextos de banco de dados e logs
SQL: prepared statements eliminam a necessidade de sanitização de saída para consultas SQL. No entanto, logs que exibem consultas executadas ainda podem ser vulneráveis.
// Correto: prepared statement
stmt = db.prepare("SELECT * FROM users WHERE id = ?")
stmt.execute([userId])
// Log seguro: escapar quebras de linha
log_message = user_input.replace("\n", "\\n").replace("\r", "\\r")
Logs: atacantes podem forjar logs para enganar sistemas de monitoramento ou ocultar atividades maliciosas.
// Log forging: entrada contém quebras de linha
// Entrada maliciosa: "Usuário admin\n[INFO] Login bem-sucedido"
// Log resultante (inseguro):
// [ERROR] Usuário admin
// [INFO] Login bem-sucedido
// Log seguro: escapar caracteres de controle
safe_input = input.replace(/[\x00-\x1F\x7F]/g, '')
NoSQL: operadores especiais como $ e . em chaves de documentos MongoDB podem causar injeção.
// Entrada maliciosa: { "$gt": "" }
// Escapar operadores especiais em chaves:
safe_key = user_key.replace("$", "\\$").replace(".", "\\.")
4. Armadilhas comuns na sanitização de saída
Dupla codificação: sanitizar dados que já foram escapados anteriormente.
// Problema: dados já escapados no banco
// Dados no banco: <script>
// Sanitização adicional: &lt;script&gt;
// Resultado: o usuário vê o código HTML literal em vez do script
// Solução: rastrear se os dados já foram escapados
// ou usar contexto único de escape na saída
Sanitização genérica: usar htmlspecialchars() para JavaScript.
// Erro: usar escape HTML para contexto JS
let name = "João & Maria";
// htmlspecialchars(name) → "João & Maria" (quebra quando usado em JS)
// Correto: usar escape específico para JS
let safeName = escapeJavaScript(name); // → "João \x26 Maria"
Ignorar atributos HTML: sanitizar conteúdo textual, mas deixar atributos vulneráveis.
// Vulnerável: atributo href sem validação
<a href="{{ userLink }}">Clique aqui</a>
// Se userLink = "javascript:alert(1)", o ataque funciona
// Correto: validar protocolo e escapar atributo
if (userLink.startsWith("http://") || userLink.startsWith("https://")) {
safeLink = escapeAttribute(userLink);
}
5. Técnicas avançadas: contextos aninhados e dinâmicos
HTML dentro de JavaScript: escapar para o contexto externo antes do interno.
// Contexto aninhado: HTML dentro de JavaScript
let template = `<div>${userContent}</div>`;
document.getElementById('container').innerHTML = template;
// Correto: escapar userContent para HTML primeiro
let safeContent = escapeHtml(userContent);
let template = `<div>${safeContent}</div>`;
JSON em HTML: serializar com JSON.stringify e depois escapar para HTML.
// Dados: { "name": "João <script>" }
let jsonData = JSON.stringify(data);
// Resultado: {"name":"João <script>"}
// Ainda vulnerável se inserido diretamente em HTML
// Correto: escapar após stringify
let safeJson = escapeHtml(JSON.stringify(data));
Conteúdo rico (Markdown, BBCode): usar parsers especializados.
// Erro: converter Markdown manualmente
let html = markdownToHtml(userInput); // Pode gerar XSS
// Correto: usar parser com sanitização integrada
let html = marked.parse(userInput, {
sanitize: true,
allowedTags: ['p', 'b', 'i', 'a'],
allowedAttributes: { 'a': ['href'] }
});
6. Ferramentas e boas práticas para o dia a dia
Bibliotecas recomendadas:
// JavaScript: DOMPurify
let cleanHtml = DOMPurify.sanitize(userHtml);
// Python: Bleach
import bleach
clean_html = bleach.clean(user_html, tags=['p', 'b', 'i'])
// Java: OWASP Java Encoder
import org.owasp.encoder.Encode;
String safeHtml = Encode.forHtml(userInput);
Content Security Policy (CSP): camada extra de defesa no navegador.
// Política CSP restritiva
Content-Security-Policy: default-src 'self'; script-src 'self'
Testes automatizados: incluir payloads de XSS em testes de saída.
// Exemplo de payloads para testar
const xssPayloads = [
'<script>alert(1)</script>',
'javascript:alert(1)',
'"><script>alert(1)</script>',
'{{constructor.constructor("alert(1)")()}}'
];
xssPayloads.forEach(payload => {
const output = sanitize(payload);
assert(!output.includes('<script>'));
});
7. Checklist final: o que verificar antes de exibir qualquer dado
- [ ] Identifiquei todos os contextos de saída (HTML, JS, URL, CSS, log, JSON)?
- [ ] A função de sanitização é específica para o contexto atual?
- [ ] Os dados já foram escapados anteriormente? (evitar dupla codificação)
- [ ] Atributos HTML estão sendo tratados separadamente do conteúdo textual?
- [ ] A política CSP está configurada como camada extra de defesa?
- [ ] Os testes automatizados incluem payloads de XSS e injeção?
- [ ] O parser de conteúdo rico (Markdown, BBCode) tem sanitização integrada?
- [ ] Logs estão escapando caracteres de controle para evitar log forging?
- [ ] Prepared statements estão sendo usados para consultas SQL?
- [ ] Operadores especiais NoSQL estão sendo escapados em chaves de documentos?
Referências
- OWASP Cross-Site Scripting Prevention Cheat Sheet — Guia completo da OWASP sobre prevenção de XSS, incluindo regras de escape para cada contexto.
- DOMPurify - Sanitização de HTML no navegador — Biblioteca JavaScript para sanitização de HTML, amplamente testada e recomendada pela OWASP.
- OWASP Java Encoder Project — Biblioteca Java oficial da OWASP para codificação contextual em HTML, JavaScript, URL e CSS.
- Bleach - Sanitização de HTML em Python — Biblioteca Python para limpeza de HTML e prevenção de XSS, com suporte a tags e atributos permitidos.
- Mozilla Developer Network - Content Security Policy (CSP) — Documentação completa da MDN sobre CSP, política de segurança que mitiga falhas de sanitização.
- PortSwigger - Cross-Site Scripting (XSS) Cheat Sheet — Referência técnica com payloads de XSS e técnicas de escape para testes de segurança.
- OWASP Logging Cheat Sheet — Guia da OWASP sobre práticas seguras de logging, incluindo prevenção de log forging.