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 &lt;script&gt; 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: &lt;script&gt;alert('XSS')&lt;/script&gt;

// Caracteres a escapar:
// < → &lt;
// > → &gt;
// & → &amp;
// " → &quot;
// ' → &#x27;

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: &lt;script&gt;
// Sanitização adicional: &amp;lt;script&amp;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 &amp; 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