Skip to content
CWE-98A03:2021 – Injection

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

PHPindex.phpVulnerable
<?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.

  1. 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.

  2. 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.

  3. 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

HTTP
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.php normalises 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.txt is 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/passwd on 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 behaviourBest realistic outcomeTypical rating
Executes the fileRemote code executionCritical
Returns the bytesArbitrary file readHigh
Returns bytes, but only from one directoryLimited disclosureLow to medium
Deserializes metadata as a side effectRCE via a gadget chainHigh 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.