What File Inclusion Actually Is
An application decides which file to load based on something you control. Everything else on this site is a consequence of that one sentence.
Overview
File inclusion is what happens when an application picks a file to load using a value an attacker supplies.
That is the whole bug. The interesting part — and the part that decides whether you are writing up a low-severity disclosure or a critical RCE — is what the application does with the file once it has it.
PHP's include and require do not read a file, they run it. The contents are parsed as PHP source and executed in the current scope. So an application that lets you choose which file include receives is letting you choose which code runs. file_get_contents and readfile, by contrast, hand back bytes. Same traversal, same missing validation, completely different finding.
The distinction sounds obvious written down and is the single most common mistake in triage. See Inclusion vs Traversal.
The vulnerable pattern
<?php
// The parameter names the file. Nothing validates it, nothing maps it,
// nothing constrains where it can point.
$page = $_GET['page'] ?? 'home.php';
include($page);Two directions out of the same bug
From that one line an attacker can go two ways, and the names for them are old enough to be slightly misleading.
Remote file inclusion (RFI) means pointing the sink at a file on a host you control, so your code runs on the target. It is the more dramatic of the two and it is very nearly dead by default — allow_url_include has shipped Off since PHP 5.2.0 in 2006. See Classic RFI.
Local file inclusion (LFI) means pointing the sink at a file already on the target's disk. Nothing has to be turned on for this to work, which is why every live finding you will encounter in 2026 starts here. See Traversal Basics.
The name of this site is a historical artifact. The work is almost entirely local.
The three preconditions
All three have to hold. Getting this wrong is the most common way a report gets closed as informative.
-
A file-loading sink.
include,require,file_get_contents,fopen, a template loader, a static-file handler. If nothing opens a file, there is no inclusion bug — you may have found something else. -
Attacker influence over the path. Complete control is the easy case. Partial control counts too: a prefix you cannot remove still lets you traverse upward, and a suffix you cannot remove is a constraint to work around rather than a fix. See Extension Append Bypasses.
-
Something worth reaching. A traversal that can only ever reach files inside a directory of public templates is not a finding. What makes it one is the config file, the environment, the session store, or an execute path — see Methodology.
Notice what is not on the list: a specific PHP version, a permissive php.ini, or a writable directory. Those decide how far you can escalate, not whether the bug exists.
What the request looks like
GET /index.php?page=../../../../etc/passwd HTTP/1.1
Host: target.example
User-Agent: Mozilla/5.0
Accept: text/html
What is not a file inclusion finding
These come up constantly and none of them is the bug on its own:
- A traversal that resolves inside the intended directory.
?page=../pages/home.phpnormalises back to where it started. You have demonstrated that..is not filtered, which is worth a sentence, not a finding. - Reading a file the web server already serves. If
/config.txtis fetchable directly, reading it through a traversal adds nothing. - A download endpoint that returns any file it is meant to return. Check the authorization boundary before assuming the path is the problem.
/etc/passwdon a container. It is real, and on a distroless or scratch image it may be the only interesting file on the box. Proving arbitrary read still matters — but say what else you could reach, or the impact section writes itself as "none".- A stack trace that leaks a filesystem path. Useful for building the traversal. Not the traversal.
And one that is a finding but frequently gets rated wrong: an inclusion sink reachable only by an authenticated administrator is still an inclusion sink. It is a privilege-escalation path from admin to the host, which is often a bigger step than it sounds.
Rating the impact
File inclusion inherits its impact from two things: what the sink does with the file, and what is on the disk.
| Sink behaviour | Best realistic outcome | Typical rating |
|---|---|---|
| Executes the file | Remote code execution | Critical |
| Returns the bytes | Arbitrary file read | High |
| Returns bytes, but only from one directory | Limited disclosure | Low to medium |
| Deserializes metadata as a side effect | RCE via a gadget chain | High to critical |
The last row is the one people miss — see Phar Deserialization, where a read-only sink is still an execute path.
Two modifiers that belong in the vector and usually get left out. Attack complexity is high when the attack needs a non-default configuration: classic RFI needs allow_url_include=On, and rating that as though it were the default overstates it every time. And a read primitive is not automatically arbitrary: the worker runs as www-data, not root, so /etc/shadow is not in scope no matter how good the traversal is.
Related
Classic RFI has been off by default since 2006 and the null byte died in 2010. Here is what replaced them, and why most material on this topic is describing a target that no longer exists.
The same missing validation, the same payload, and two findings with different severities and different fixes. Whether the sink executes is the only thing that separates them.
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.
The textbook payload: point the include at a file on your own host. It requires a configuration PHP stopped shipping in 2006, and it is still the first thing everyone tries.
An order of operations that answers the questions that change what you do next, before spending time on payloads that the answers would have ruled out.