In PHP, why does building an include/require path from request input risk code execution, and what do allow_url_include and basename() each actually stop?
answer
- request-controlled path selects executed file
- local inclusion runs uploaded PHP too
- allow_url_include default Off blocks remote URLs
- php://filter can leak source locally
- basename is naive, not '..'-aware
basics
~20 sinclude/require runs the file at the path you give, so request input selects the executed file: a planted local file runs as PHP, or a remote URL runs if allow_url_include is on. allow_url_include (default Off) blocks only the URL case. Use an allow-list, not basename().
solid answer
~50 s`include`/`require` run the file at the resolved path, so a request-controlled path is **file inclusion**. *Remote* inclusion is gated by **`allow_url_include`, which defaults to `Off`** (and requires `allow_url_fopen`; it is deprecated in PHP 8.5). But *local* inclusion is not gated: if an attacker can plant a file (an upload disguised as an image, a log line they control) and steer its local path into the `include`, its PHP executes. Even a pure read via `php://filter/convert.base64-encode/...` — a **local** wrapper, not blocked by `allow_url_include` — leaks source. `basename($path)` strips the directory but the manual notes it works **naively on the string, unaware of `..` or the filesystem**; it does not stop the attacker choosing among files in one directory. The real control is an **allow-list**: map request tokens to fixed paths, and never concatenate input into an include path.
go deeper
Know that include/require runs the target file and that a user-controlled path is dangerous.
Explain LFI vs RFI, that allow_url_include defaults Off and gates only the URL case, and that basename is a naive string function.
Diagnose an inclusion bug, distinguish local wrappers like php://filter from URL wrappers, and replace the sink with an allow-list plus safe upload handling.
Set org policy on dynamic includes and upload storage, and decide where path resolution is enforced versus designed out.
## Why an include path from input is code execution `include`, `require`, and their `_once` forms compile and run the target file as PHP. Whatever chooses the path chooses the program. When that choice comes from `$_GET`, `$_POST`, a header, or a cookie, you have a **file inclusion** vulnerability. It splits into two shapes: - **Local file inclusion (LFI):** the path resolves to a file already on the server. If that file contains PHP the attacker influenced — an uploaded avatar with `<?php` inside, a session file, a log they can write to — including it runs their code. Even a pure read is a leak: templates, `.env`-style config, or source. - **Remote file inclusion (RFI):** the path is a URL, so PHP fetches and runs remote code. This is the most direct RCE and the reason the URL case is disabled by default. ## The two directives, precisely | Directive | Default | Scope | What it gates | |---|---|---|---| | `allow_url_fopen` | `On` | `PHP_INI_SYSTEM` | whether URL wrappers work in file functions at all | | `allow_url_include` | `Off` | `PHP_INI_SYSTEM` | whether `include`/`require` may use a URL wrapper (needs `allow_url_fopen` on) | So on a default install RFI via `http://` is already blocked, and `allow_url_include` is **deprecated in PHP 8.5** — you should never turn it on. What that default does **not** block: - `php://filter/...` — a **local** stream wrapper (`is_url = 0` in the engine), so it works even with `allow_url_include` off, and `convert.base64-encode` hands the attacker readable source of any file the process can open. - `data://` and `php://input` — these *are* URL-ish wrappers (`data://` is `is_url = 1`), so they need `allow_url_include` to inject code, which the default denies. - Any purely **local** path — an uploaded file, `/tmp` content, a poisoned log — because those never touch a URL wrapper. ## Why `basename()` is not the fix `basename($path)` returns the trailing name component, which tempts developers to write `include('pages/' . basename($_GET['page']) . '.php')`. But: - The manual states `basename` **operates naively on the input string and is not aware of the filesystem or of `..`** — it is a string trimmer, not a path validator. - Even done correctly, stripping the directory only pins the *folder*; the attacker still chooses **which file in that folder** runs. If that folder holds anything includable that they can influence, you still lose. - It says nothing about wrappers, null bytes (fixed since 5.3.4 but a historical trap), or symlinks. ## The control that works 1. **Allow-list.** Keep a fixed map: `['home' => 'pages/home.php', 'about' => 'pages/about.php']`. Look the request token up; if it is absent, 404. The input never becomes part of a path. 2. **Never concatenate input into an include argument.** If a directory of templates must be dynamic, resolve with `realpath()` (owned by the files-directories leaf) and verify the result is inside the intended base *before* including — but an allow-list is simpler and stronger. 3. **Keep uploads out of the include path** and non-executable, so a "local file" can't become "local code." The theory of traversal and canonicalisation lives in found-appsec-path-traversal; here the PHP-specific facts are the two directives' defaults and scope, the local-vs-URL wrapper split, and that `basename` is a string function, not a guard.
- With `allow_url_include` off, is `include($_GET['p'])` safe?No. That directive only blocks the *remote URL* case. Local inclusion still runs any PHP the attacker can place on disk, and `php://filter` (a local wrapper) can still read source. Concatenating request input into an include path is unsafe regardless of the directive.
- Why prefer an allow-list over `realpath()` + a prefix check?Both can work, but an allow-list keeps input out of the path entirely, so there is nothing to canonicalise and no TOCTOU window between the check and the include. A prefix check must assume `realpath` resolved symlinks the way you expect and that nothing changes between check and use — more moving parts for the same goal.
- How can an uploaded image lead to inclusion RCE?If uploads are stored under a known path and an attacker uploads a file whose bytes contain `<?php ...` (a valid image can carry it), then steers that path into an `include`, PHP runs the embedded code. Storing uploads outside any include path and serving them as static, non-PHP content prevents it.
saying these in an interview costs you the question
- allow_url_include off means include of user input is safe
- basename() sanitises a path against traversal
- only remote URLs are dangerous; local includes are fine
- php://filter is blocked by allow_url_include
- adding a fixed '.php' suffix makes the path safe
- an image upload can never contain executable PHP