Encoding and bypass
techniqueTransformations you apply to a payload that already works. Nothing here is worth using until you know which control you are trying to get past.
01The decode chain
The only concept on this page. Everything else is an application of it.
A request passes through several components before it reaches the code you are attacking. Each one may decode, normalise, or reject. A bypass works when one component sees a harmless string and a later one sees the payload.
Two failure modes produce every bypass on this page:
- The decode count differs. The WAF decodes once, the framework decodes twice.
%2527is harmless text to the WAF and a quote to the application. - Normalisation differs. The proxy treats
..;/as a literal directory name; Tomcat strips the path parameter and resolves it as../.
Map the chain before guessing. Take one harmless character, send it through every encoding, and read what comes back.
<, the chain decodes twice.<, something normalises unicode.Record the answer once per application, not once per finding. It is the same for every parameter, and it decides every bypass you attempt afterwards. Ten seconds here saves an hour of guessing later.
02URL encoding
What it is — % followed by two hex digits. The base transformation everything else builds on.
What it defeats — literal string matching on the raw request. A filter looking for ' doesn't see %27 if it inspects before decoding.
Where it fails — most current WAFs decode once before inspecting, so plain single encoding rarely helps on its own. Its real value is targeted: encode only the character the filter keys on and leave the rest readable so the payload still parses at the destination.
' | %27 |
" | %22 |
< | %3c |
> | %3e |
/ | %2f |
\ | %5c |
. | %2e |
; | %3b |
& | %26 |
= | %3d |
# | %23 |
% | %25 |
| space | %20 |
| NUL | %00 |
| CR / LF | %0d / %0a |
| TAB | %09 |
Two details worth remembering. + decodes to a space in a query string and in a form body, but not in the path — /api/a+b is a literal plus. And %2f in the path is a literal slash to some proxies and a separator to others, which is a traversal bypass and an access-control bypass on its own.
03Double and multiple encoding
For a chain that decodes more times than the filter does.
1=1 while the quote still terminates the string at the database.' | %27 → %2527 → %252527 |
< | %3c → %253c → %25253c |
../ | %2e%2e%2f → %252e%252e%252f |
\ | %5c → %255c |
Partial beats full. Encoding everything usually breaks the payload at the destination. Encode the one character the filter is looking for.
04Unicode
Four separate mechanisms get called "unicode". They defeat different things.
\uXXXX works in identifiers as well as strings, so this is a valid call to alert.Defeats — signature matching on a raw JSON body. The bytes ' don't match a rule looking for ', but the JSON parser hands the application a real quote. This is the most reliable WAF bypass against JSON APIs, because many deployments inspect JSON bodies as flat text.
Fails against — anything inspecting after JSON parsing, and anything sent as a form body, where ' stays literal.
<>'%Fullwidth only works where the application normalises input after the filter runs, which is a minority of them. Confirm with the chain probe rather than assuming. Other pairs worth knowing: K (U+212A) normalises to K, and ff (U+FB00) expands to ff — changing the string length after the length check ran.
05Case and comments
A correction to a common mistake. /**/ splits SQL tokens because the SQL lexer treats a comment as whitespace. It does not work inside a JavaScript identifier — ale/**/rt is two identifiers and a syntax error. For JavaScript use alert or window['ale'+'rt'].
06Parser differentials
The highest-value category, because these bypass a control completely rather than obscuring a payload.
| PHP | Last |
| Servlet containers | First, from getParameter |
| ASP.NET | Both, joined with a comma |
| Express | An array |
| Flask | First, from request.args.get |
When a front-end control reads one and the application reads the other, that is a bypass. Reflect the value back to confirm which behaviour you're facing before relying on it.
07Which encoding defeats which control
Read this starting from the control you observed, not from the encoding you like.
| Literal match on the raw request | Single URL encoding, case variation |
| WAF decodes once, framework twice | Double URL encoding |
| Signature matching a JSON body | \uXXXX inside string values |
| Keyword blocklist | Character construction — CHAR, CHR, 0x, concatenation |
| Case-sensitive blocklist | Mixed case |
| Whitespace stripped or blocked | /**/, parentheses, ${IFS}, %09 |
| Filter runs before normalisation | Fullwidth characters |
| Proxy path allowlist | ..;/, %2e%2e%2f, //, /./, trailing %20 |
| Front-end validates, backend re-parses | Duplicate parameters, keys, headers |
| Inspection size cap | A large benign prefix before the payload |
08Rules for using this page
- One transformation at a time. If you encode, case-vary and comment-split at once and it works, you've learned nothing you can reuse.
- Start from a working payload. Get it working with no filter in the way — locally, or on an unfiltered parameter — then transform it. Blind-firing transformed payloads produces results you cannot interpret.
- A bypass you don't understand is a bypass you can't adapt. When something works, decide which of the two mechanisms at the top it exploited. That transfers to the next endpoint. The specific string does not.
- Record the working transformation once per application, not per finding. It will apply to everything else you test there.