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
Double encoding, which is the one that works
# 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/passwdOverlong 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.
Related
A filter that removes bad strings has to anticipate every spelling. Resolution has to anticipate nothing. That asymmetry is why str_replace loses.
include($page . '.php') is the most common shape of this bug, and the two famous bypasses for it both died in 2010. Here is what is actually left.
Traversal works because of specific, boring rules about how a string becomes a file. Knowing them turns guessing at ../ counts into arithmetic.
The technique that needs no configuration, no flag, and no version. Nothing in any php.ini stops it, which is why every live finding in 2026 starts here.
An allowlist is the correct fix, so most of them are not really allowlists. Here is how to tell, and what a prefix does and does not stop.