File Inclusion: LFI, RFI, and the Road to RCE
Preventing File Inclusion (LFI and RFI)
How to prevent local and remote file inclusion: allow-list view mapping, keep user input out of include, disable URL includes, and least privilege.
Published
File inclusion has one real fix and several supporting controls. The real fix is to never let user input choose the file that include loads. Everything else reduces blast radius for when something slips through. This page is the defensive reference for the cluster. Want to see why the weak controls fail? Practice the bypasses on a live target.
The one control that matters: map, do not concatenate
Accept a key and look up the real path from a fixed table. User input then selects from a known set and never becomes a path:
// safe: the parameter picks a key, never a path
$pages = [
'home' => 'home.php',
'about' => 'about.php',
'contact' => 'contact.php',
];
$key = $_GET['page'] ?? 'home';
include $pages[$key] ?? 'home.php'; // unknown key falls back, never reaches the filesystem rawBecause the parameter can only ever be a key into $pages, no traversal, wrapper, URL, or null byte reaches include. This single pattern defeats every technique in this cluster at once.
Allow-list the view names
If you cannot use a lookup table, validate the value against an explicit allow-list of permitted names and reject anything else. Allow-list, do not blacklist: filtering out ../ is bypassable with nesting, encoding, and wrappers, while an allow-list of known-good names has nothing to bypass.
Know why the weak controls fail
These show up constantly and none of them is sufficient:
basename()stops some directory traversal but not wrapper prefixes, same-directory targets, or appended-extension abuse.str_replace("../", "")is defeated by nested sequences like....//that reassemble after one non-recursive pass.- Appending a fixed extension is defeated by wrappers that resolve their resource regardless of the suffix, and by null bytes on legacy PHP.
- WAF keyword blocks on
php://oretc/passwdare a speed bump: encoding, alternate wrappers, and different targets walk around them.
Disable URL includes
Turn off the settings that enable remote and wrapper-based code injection:
allow_url_include = Off kills RFI and data:// / php://input as code
allow_url_fopen = Off stops remote fetches entirely where the app does not need themThis closes the remote half of the problem. It does not stop local inclusion, so it is a supporting control, not the fix.
Jail the application and drop privileges
Shrink what a successful inclusion can reach:
open_basedirconfines PHP file access to a set of directories, keeping/proc,/var/log, and other apps' files out of reach of an include.- Non-executable upload, log, and session directories mean a poisoned file cannot run even if it is included.
- Least privilege for the web user: it should not read
/etc/shadow, SSH keys, or other applications' session files.
The layered picture
Think of it as one real control plus containment:
- Primary: user input never selects the included file (map a key to a fixed path, or allow-list names).
- Supporting:
allow_url_include/allow_url_fopenoff,open_basedirset, sensitive directories non-executable, web user least-privileged.
With the primary control in place, the supporting ones are defense in depth. Without it, the supporting ones only slow an attacker down.
For the attack side of each of these, work back through the cluster from the pillar, and keep the cheat sheet handy. Ready to prove the weak controls really do fall? Sign up free and try it.