Back

Prevenindo XSS em Conteúdo Gerado por Usuários

Prevenindo XSS em Conteúdo Gerado por Usuários

Ataques de cross-site scripting (XSS) através de conteúdo gerado por usuários continuam sendo uma das ameaças de segurança mais persistentes enfrentadas por aplicações web. Seja construindo um sistema de comentários, manipulando envios de formulários ou implementando editores de texto rico, qualquer funcionalidade que aceita e exibe entrada de usuário cria vulnerabilidades potenciais de XSS. Frameworks JavaScript modernos fornecem proteções integradas, mas suas válvulas de escape e a complexidade de aplicações do mundo real significam que desenvolvedores devem entender e implementar técnicas adequadas de prevenção de XSS.

Este artigo cobre as estratégias essenciais para prevenir XSS em conteúdo gerado por usuários: validação e normalização de entrada, codificação de saída consciente do contexto, manipulação segura de conteúdo rico e controles suplementares de defesa em profundidade. Você aprenderá por que a validação por lista de permissões supera a filtragem por lista de negação e como aproveitar os padrões dos frameworks evitando armadilhas comuns de segurança.

Principais Conclusões

  • Sempre valide entrada de usuário usando listas de permissões, não listas de negação
  • Aplique o método de codificação correto para cada contexto de saída (HTML, JavaScript, CSS, URL)
  • Use DOMPurify ou bibliotecas similares para sanitizar conteúdo HTML rico
  • Aproveite os padrões dos frameworks e evite válvulas de escape a menos que absolutamente necessário
  • Implemente defesa em profundidade com cabeçalhos CSP e atributos seguros de cookies
  • Teste suas medidas de prevenção de XSS com testes automatizados de segurança

Entendendo Riscos de XSS em Conteúdo Gerado por Usuários

Conteúdo gerado por usuários apresenta desafios únicos de XSS porque combina entrada não confiável com a necessidade de funcionalidades dinâmicas e interativas. Sistemas de comentários, perfis de usuários, avaliações de produtos e ferramentas de edição colaborativa todos requerem aceitar conteúdo similar ao HTML enquanto previnem execução maliciosa de scripts.

Frameworks modernos como React, Angular e Vue.js lidam com prevenção básica de XSS automaticamente através de seus sistemas de templates. No entanto, essas proteções se quebram quando desenvolvedores usam válvulas de escape dos frameworks:

  • dangerouslySetInnerHTML do React
  • Métodos bypassSecurityTrustAs* do Angular
  • Diretiva v-html do Vue
  • Manipulação direta do DOM com innerHTML

Essas funcionalidades existem por razões legítimas—exibir conteúdo formatado, integrar widgets de terceiros ou renderizar HTML criado por usuários. Mas cada bypass cria um vetor potencial de XSS que requer manipulação cuidadosa.

Validação de Entrada: Sua Primeira Linha de Defesa

Implementando Validação por Lista de Permissões

Validação por lista de permissões define exatamente qual entrada é aceitável, rejeitando todo o resto por padrão. Esta abordagem se mostra muito mais segura que filtragem por lista de negação, que tenta bloquear padrões perigosos conhecidos.

Para dados estruturados como endereços de email, números de telefone ou códigos postais, use expressões regulares rigorosas:

// Validação por lista de permissões para CEPs brasileiros
const cepPattern = /^\d{5}-?\d{3}$/;

function validateCEP(input) {
  if (!cepPattern.test(input)) {
    throw new Error('Formato de CEP inválido');
  }
  return input;
}

Por Que Filtros de Lista de Negação Falham

Abordagens de lista de negação que tentam filtrar caracteres perigosos como <, > ou tags script inevitavelmente falham porque:

  1. Atacantes facilmente contornam filtros usando codificação, variações de maiúsculas/minúsculas ou peculiaridades do navegador
  2. Conteúdo legítimo é bloqueado (como “O’Brien” ao filtrar apostrofes)
  3. Novos vetores de ataque emergem mais rápido do que listas de negação podem ser atualizadas

Normalizando Unicode e Texto de Forma Livre

Para conteúdo gerado por usuários que inclui texto de forma livre, implemente normalização Unicode para prevenir ataques baseados em codificação:

function normalizeUserInput(text) {
  // Normalizar para forma NFC
  return text.normalize('NFC')
    // Remover caracteres de largura zero
    .replace(/[\u200B-\u200D\uFEFF]/g, '')
    // Remover espaços em branco nas extremidades
    .trim();
}

Ao validar texto de forma livre, use lista de permissões de categoria de caracteres em vez de tentar bloquear caracteres perigosos específicos. Esta abordagem suporta conteúdo internacional mantendo a segurança.

Codificação de Saída Consciente do Contexto

Codificação de saída transforma dados do usuário em um formato seguro para exibição. A percepção chave: diferentes contextos requerem diferentes estratégias de codificação.

Codificação de Contexto HTML

Ao exibir conteúdo de usuário entre tags HTML, use codificação de entidades HTML:

function encodeHTML(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

// Seguro: conteúdo do usuário é codificado
const userComment = "<script>alert('XSS')</script>";
element.innerHTML = `<p>${encodeHTML(userComment)}</p>`;
// Renderiza como: <p>&lt;script&gt;alert('XSS')&lt;/script&gt;</p>

Codificação de Contexto JavaScript

Variáveis colocadas em contextos JavaScript requerem codificação hexadecimal:

function encodeJS(str) {
  return str.replace(/[^\w\s]/gi, (char) => {
    const hex = char.charCodeAt(0).toString(16);
    return '\\x' + (hex.length < 2 ? '0' + hex : hex);
  });
}

// Seguro: caracteres especiais são codificados em hex
const userData = "'; alert('XSS'); //";
const script = `<script>var userName = '${encodeJS(userData)}';</script>`;

Codificação de Contexto CSS

Dados de usuário em CSS requerem codificação específica para CSS:

function encodeCSS(str) {
  return str.replace(/[^\w\s]/gi, (char) => {
    return '\\' + char.charCodeAt(0).toString(16) + ' ';
  });
}

// Seguro: codificação CSS previne injeção
const userColor = "red; background: url(javascript:alert('XSS'))";
element.style.cssText = `color: ${encodeCSS(userColor)}`;

Codificação de Contexto URL

URLs contendo dados de usuário precisam de codificação percentual:

// Use codificação integrada para parâmetros de URL
const userSearch = "<script>alert('XSS')</script>";
const safeURL = `/search?q=${encodeURIComponent(userSearch)}`;

Manipulando Conteúdo Rico com Segurança

Muitas aplicações precisam aceitar conteúdo HTML rico de usuários—posts de blog, descrições de produtos ou comentários formatados. Codificação simples quebraria a formatação, então você precisa de sanitização HTML.

Usando DOMPurify para Sanitização HTML

DOMPurify fornece sanitização HTML robusta que remove elementos perigosos preservando formatação segura:

import DOMPurify from 'dompurify';

// Configure DOMPurify para suas necessidades
const clean = DOMPurify.sanitize(userHTML, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
  ALLOWED_ATTR: ['href', 'title'],
  ALLOW_DATA_ATTR: false
});

// Seguro para inserir HTML sanitizado
element.innerHTML = clean;

Padrões Seguros Específicos por Framework

Cada framework tem padrões preferidos para manipular conteúdo gerado por usuários com segurança:

React:

import DOMPurify from 'dompurify';

function Comment({ userContent }) {
  const sanitized = DOMPurify.sanitize(userContent);
  return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}

Vue.js:

<template>
  <div v-html="sanitizedContent"></div>
</template>

<script>
import DOMPurify from 'dompurify';

export default {
  computed: {
    sanitizedContent() {
      return DOMPurify.sanitize(this.userContent);
    }
  }
}
</script>

Angular:

import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import DOMPurify from 'dompurify';

export class CommentComponent {
  constructor(private sanitizer: DomSanitizer) {}
  
  getSafeContent(content: string): SafeHtml {
    const clean = DOMPurify.sanitize(content);
    return this.sanitizer.bypassSecurityTrustHtml(clean);
  }
}

Controles de Defesa em Profundidade

Embora codificação e sanitização adequadas forneçam proteção primária, controles adicionais adicionam camadas de segurança:

Content Security Policy (CSP)

Cabeçalhos CSP restringem quais scripts podem executar, fornecendo uma rede de segurança contra XSS:

// Exemplo Express.js
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'nonce-" + generateNonce() + "'"
  );
  next();
});

Atributos Seguros de Cookies

Defina flags HttpOnly e Secure em cookies para limitar o impacto de XSS:

res.cookie('session', sessionId, {
  httpOnly: true,  // Previne acesso JavaScript
  secure: true,    // Apenas HTTPS
  sameSite: 'strict'
});

Testes e Validação

Implemente testes automatizados para capturar vulnerabilidades XSS:

// Exemplo de teste Jest
describe('Prevenção XSS', () => {
  test('deve codificar HTML em comentários', () => {
    const malicious = '<script>alert("XSS")</script>';
    const result = renderComment(malicious);
    expect(result).not.toContain('<script>');
    expect(result).toContain('&lt;script&gt;');
  });
});

Conclusão

Prevenir XSS em conteúdo gerado por usuários requer uma abordagem multicamadas. Comece com validação de entrada por lista de permissões e normalização, aplique codificação de saída consciente do contexto baseada em onde os dados serão exibidos, e use bibliotecas comprovadas como DOMPurify para sanitização de conteúdo rico. Embora frameworks modernos forneçam excelentes proteções padrão, entender quando e como usar suas válvulas de escape com segurança permanece crítico. Lembre-se de que filtragem por lista de negação sozinha nunca fornecerá proteção adequada—foque em definir o que é permitido em vez de tentar bloquear cada padrão de ataque possível.

Perguntas Frequentes

Use uma biblioteca de sanitização HTML bem mantida como DOMPurify. Configure-a para permitir apenas tags seguras como b, i, em, strong, a e p enquanto remove tags script, manipuladores de eventos e atributos perigosos. Sempre sanitize no lado servidor assim como no lado cliente para defesa em profundidade.

Armazene entrada de usuário em sua forma original no banco de dados e codifique-a no ponto de saída. Esta abordagem preserva os dados originais, permite que você mude estratégias de codificação posteriormente e garante que você aplique a codificação correta para cada contexto de saída.

Escapar converte todas as tags HTML para seus equivalentes de entidade, exibindo-as como texto em vez de executá-las. Sanitizar remove elementos perigosos preservando formatação HTML segura. Use escape para campos de texto simples e sanitização para editores de conteúdo rico.

Parse markdown no lado servidor usando uma biblioteca segura, então sanitize o HTML resultante com DOMPurify antes de enviá-lo ao cliente. Nunca confie apenas no parsing de markdown do lado cliente, pois atacantes podem contorná-lo enviando HTML malicioso diretamente para sua API.

Frameworks modernos previnem XSS por padrão através de escape automático, mas eles fornecem válvulas de escape como dangerouslySetInnerHTML que contornam essas proteções. Você deve garantir segurança manualmente ao usar essas funcionalidades, ao manipular arquivos enviados por usuários, ou ao construir dinamicamente URLs ou valores CSS.

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers