Skip to content
mediumCVSS 5.3CWE-22A01:2021 – Broken Access Control

Encoding Bypasses

Every encoding here targets a filter that inspects the input string. None of them targets path resolution, because resolution cannot be fooled by spelling.

What these are for

An encoding bypass works when there is a gap between the layer that checks and the layer that opens the file. The check sees one string, the filesystem sees another, and the difference is a decoding step that happened in between.

That framing matters because it tells you when to reach for one. If the application calls realpath() and compares the result to a base directory, no encoding on this page helps — resolution has already happened and there is nothing left to spell differently. If the application runs a regex over $_GET['page'], all of them are candidates.

So before trying encodings, work out what you are up against. A filter that rejects your payload with a distinct error is a string check. One that silently returns the wrong file is resolution.

The encodings

FormDecoded byBeatsRealistic in 2026?
%2e%2e%2fThe web server, before PHPA filter reading the raw query stringYes — common in proxies and WAFs
%252e%252e%252fTwo decoding layersA gateway that decodes, checks, and forwardsYes — the best of these
..%c0%afA non-conformant UTF-8 decoderDecoders that accept overlong formsNo — rejected since Unicode 3.1 (2001)
..%u2215IIS-style %u escapesOld Microsoft stacksNo — removed from modern IIS
..%00/Nothing — PHP rejects itNothingNo — dead since PHP 5.3.4 (2010)
..\Nothing; Windows accepts it nativelyA filter that only knows /Yes — on Windows targets
..%5cThe web serverSame, plus a filter checking for a literal backslashYes — on Windows targets
....//Nothing — a filter constructs itA single non-recursive replaceYes — see Beating Blacklists
UTF-16 or UTF-7 via convert.iconvThe filter wrapperA check on the response body, not the inputYes — niche

Double encoding, which is the one that works

Payload
# Single encoding is decoded by the web server before the application sees
# it. So a filter running INSIDE the application already sees ../ and
# rejects it. Single encoding only helps against something in FRONT of the
# app — a WAF or proxy matching on the raw query string.

%2e%2e%2f%2e%2e%2f%2e%2e%2fetc/passwd
  -> app receives: ../../../etc/passwd

# Double encoding survives one decode. If there are two decoding layers —
# a gateway that decodes to inspect and forwards the original, plus the app
# server — the check sees %2e%2e%2f (harmless) and the app sees ../ (not).

%252e%252e%252f%252e%252e%252fetc/passwd
  -> gateway decodes:  %2e%2e%2f%2e%2e%2fetc/passwd     <- inspected, looks fine
  -> app decodes:      ../../etc/passwd                 <- opened

# Mixed forms defeat a regex written for one shape.
%2e%2e/%2e%2e/%2e%2e/etc/passwd
..%2f..%2f..%2fetc/passwd
.%2e/.%2e/.%2e/etc/passwd

# Only the slash encoded — often enough, and shorter.
..%2f..%2f..%2fetc/passwd

Overlong UTF-8, and why it is on every list anyway

%c0%ae is a two-byte encoding of ., which UTF-8 says must be encoded in one byte. A decoder that accepts it — a non-shortest-form decoder — turns ..%c0%af into ../.

Unicode declared these ill-formed in 2001 and every conformant decoder has rejected them since. That includes glibc's iconv, Java's string constructors, .NET, Python 3, and every browser. The famous instances — the IIS Unicode directory traversal of 2000, various Java stacks — were all fixed two decades ago.

It stays on cheatsheets because it costs one request and because embedded devices and appliances sometimes ship hand-rolled decoders that never got the memo. That is the realistic target: a router web UI, a printer, an industrial controller. Not a modern Linux stack.

The payload builder emits a warning to this effect whenever you select it, for the same reason this section exists — the technique is not wrong, it is just aimed at a decade that ended.

The null byte is dead

It deserves its own section only because it is still the first suggestion in most material on this topic.

?page=../../../etc/passwd%00 worked because PHP passed the string to a C API that terminated at the first null, so an appended .php was never seen. PHP 5.3.4 fixed this in December 2010. PHP 8 raises a ValueError if a path argument contains a null byte at all.

It is dead in every other runtime too — Java since 7, Python 3 (ValueError: embedded null byte), Node (ERR_INVALID_ARG_VALUE), .NET, Ruby, Go. There is no supported version of anything on the cheatsheet where this works.

If you are testing a genuinely ancient target — an appliance on PHP 5.2, which does still exist — it is worth one request. Otherwise, when you need to defeat an appended extension, see Extension Append Bypasses for what actually remains.

Knowing which layer decodes

The whole technique depends on this, so it is worth being precise.

Apache and nginx decode the path once before the request reaches PHP. $_GET values are decoded once by PHP's own query-string parser. So a single-encoded payload in a query parameter arrives at your PHP code fully decoded — a filter in the application sees ../.

A WAF or reverse proxy may decode to inspect and then forward the original bytes, giving you two decodes on the app side and one on the inspection side. That asymmetry is exactly what double encoding exploits.

Path segments behave differently from query parameters. If the vulnerable value comes from PATH_INFO or a rewritten URL rather than $_GET, the web server's own path normalisation applies first — and Apache and nginx normalise .. in the path before rewriting. That is why the same payload can work in ?page= and fail in /page/.

When in doubt, send a distinctive marker and read it back through a source disclosure or an error message. One observation beats four assumptions.