Local File Inclusion (LFI) and Remote File Inclusion (RFI) happen when an application uses unsanitised user input to build a file path passed into include(), require(), or an equivalent dynamic-loading function. LFI lets an attacker pull in files already on the server — configuration files, source code, log files, or /proc entries — often escalating to remote code execution (RCE). RFI, a narrower and now rarer case (mainly older PHP with allow_url_include enabled), lets an attacker force the server to fetch and execute a file from a URL they control. Both bugs trace back to one root cause: trusting user-supplied input as part of a file path. For Indian dev teams shipping PHP, Node, Python, or Java apps, this is still one of the most common OWASP Top 10 (A03: Injection / A01: Broken Access Control) findings in real-world code review — and one of the cheapest to prevent.
How LFI Actually Works
Most LFI bugs start with an innocent-looking pattern: a page parameter that selects which file to load, like ?page=about mapping to include($_GET['page'] . '.php'). If the parameter is not restricted, an attacker overrides it with a path to a file the developer never intended to expose.
The classic escalation path looks like this:
- Disclosure — read files the app was never meant to serve:
../../../../etc/passwd, database config files,.envfiles, or source code that reveals other vulnerabilities. - Path traversal —
../sequences walk the directory tree upward past the intended base folder, bypassing a naive "look inside this one folder" assumption. - Log poisoning — an attacker injects PHP code into a file the server already writes to and can read back, such as the web server access log or a session file, by sending a crafted
User-Agentheader, then includes that log file to execute the injected code. - PHP wrapper abuse — PHP's
php://filter,php://input,data://, andexpect://wrappers let an attacker read source as base64, inject raw code via POST body, or execute commands directly, turning a "read-only" disclosure bug into full RCE. /proctricks — on Linux,/proc/self/environor/proc/self/fd/*can expose environment variables (secrets, keys) or open file descriptors, sometimes enabling code execution when combined with an injectable environment variable like a maliciousUser-Agent.
Why RFI Is Rarer But Still Worth Knowing
RFI requires the target language's include function to be able to fetch remote content over a URL rather than only reading local disk paths. In PHP this depends entirely on the allow_url_include setting being enabled — it has been disabled by default since PHP 5.2, which is why RFI has become uncommon in current environments even though the related allow_url_fopen setting (which governs remote reads via fopen()/file_get_contents(), not includes) is still on by default in a stock install. Where RFI still shows up is legacy PHP applications, misconfigured shared hosting, or older CMS plugins that had allow_url_include explicitly re-enabled and were never re-audited after a server migration. When RFI does work, it is often more severe than LFI because the attacker controls the entire payload rather than being limited to files already present on the server.
require() with a dynamic, user-influenced path, Python's importlib with attacker-controlled module names, and Java's classloader-based dynamic includes all have equivalent LFI-style risk when fed unsanitised input.Local vs Remote File Inclusion at a Glance
| Aspect | LFI | RFI |
|---|---|---|
| File source | Already present on the server | Fetched from an attacker-controlled URL |
| Typical precondition | Unsanitised path in include()/require() | Above, plus allow_url_include enabled |
| Common outcome | Disclosure, path traversal, log/wrapper-based RCE | Direct RCE via attacker's remote payload |
| Prevalence today | Common, still found in code review | Rare on modern PHP defaults |
| Primary fix | Allow-list of valid values, never pass raw input to include | Disable allow_url_include/allow_url_fopen, same allow-list |
Know your vulnerabilities before attackers do
Run a free VAPT scan — takes 5 minutes, no signup required.
Book Your Free ScanReal-World Signals: Why This Still Matters
Code review teams that assess Indian SaaS and fintech codebases regularly find LFI-class bugs in file-download endpoints, template selectors, plugin loaders, and multi-tenant "theme" or "report" pickers — features built for legitimate flexibility that never had their input constrained. The pattern is rarely a single obvious include($_GET[...]) line; it is usually one indirection layer removed, inside a helper function that "just builds a path," making it easy to miss in a quick read and exactly the kind of finding a structured VAPT engagement is built to catch.
Catching File Inclusion Before It Ships
Static analysis alone under-catches this class of bug, because the vulnerable pattern is rarely a single obvious line — it usually lives inside a helper function that concatenates a base directory with a caller-supplied value several calls away from the eventual include() or require(). Effective detection combines three layers, and dropping any one of them leaves a gap:
- Manual code review with a specific checklist item. Flag every place a variable derived from request data — query string, header, cookie, uploaded filename, or a value pulled from a database that itself originated from user input — reaches a dynamic-loading function, even indirectly through a wrapper.
- Dynamic testing with traversal and wrapper payloads. Attempt
../sequences with varying encodings, PHP stream wrappers such asphp://filter/convert.base64-encode/resource=, and null-byte variants against every parameter that influences which file, template, or module gets loaded — not only the ones that look obviously file-related. - Error-message review. A stack trace or a "failed to open stream" error that echoes back the attempted file path confirms the input reaches a filesystem call, even before a successful inclusion is achieved; this is often the first signal a tester gets, well before full exploitation.
Defence: The Only Fixes That Actually Hold
The fix for this vulnerability class is not "sanitise better" — string-blacklisting ../ or null bytes has a long history of being bypassed with encoding tricks, double encoding, and wrapper prefixes. The fixes that actually hold are structural:
- Never pass user input into
include()/require()(or their equivalents) at all. If the feature genuinely needs to select between a fixed set of files, map user input to an internal identifier first. - Use an allow-list, not a deny-list. Maintain an explicit array of permitted values (
['home', 'about', 'contact']) and reject anything not in it, rather than trying to strip dangerous characters. - Disable
allow_url_includeandallow_url_fopenin PHP configuration unless a specific, reviewed feature requires remote fetches — this alone removes the RFI class entirely. - Validate and canonicalise paths server-side — resolve the final absolute path and confirm it still sits inside the intended base directory before opening it, rejecting anything that resolves outside it.
- Run the web server process with least privilege and restrict
open_basedir(PHP) or equivalent filesystem sandboxing so even a successful traversal cannot reach unrelated system files. - Don't rely on file extensions or content-type checks as security controls — an attacker-controlled log or session file with a
.logor.txtextension is still interpreted as code once included.
php://filter/php://input abuse are the two techniques manual code review most often misses, because they require reasoning about what an included file becomes after user-controlled data lands in it — not just where the include path comes from. Combine static code review with dynamic testing (a VAPT pass that actually attempts inclusion payloads) rather than relying on either alone.Fitting This Into Your SDLC
File inclusion bugs are a textbook example of why secure code review needs to happen before a feature ships, not after a customer reports it. Static analysis catches the obvious include($_GET[...]) pattern; it routinely misses the indirect, helper-function version, and it never catches log-poisoning or wrapper-chaining because those require actually attempting the exploit chain. Indian dev teams building anything that dynamically loads templates, plugins, reports, or user-selectable content should treat this as a standing code-review checklist item, and validate it periodically with a proper penetration test rather than a one-time review. This class of check runs as part of Dhisattva AI Pvt Ltd's automated and manual VAPT methodology, and for regulated sectors needing a formal certificate, the deeper manual pass is delivered with a CERT-In empanelled partner.
Teams that want this checked against their own codebase can start with a free VAPT scan, and map any file-handling findings into a DPDP-aligned remediation plan via the DPDP compliance guidance. More secure-coding methodology is on the Bachao.AI blog.
Sources
Frequently Asked Questions
What is the difference between LFI and path traversal?
../ sequences to escape an intended directory); LFI is the vulnerability class that results when that traversed path is passed into an include()/require() function and executed, not just read. Path traversal can exist without LFI if the file is only read and returned, not included as code.Can LFI lead to remote code execution without RFI being possible?
php://filter, php://input), and session file injection all achieve code execution using files already on the server, with no remote fetch required, which is why LFI alone is treated as a critical-severity finding.Does disabling allow_url_include fully fix this vulnerability class?
Why do blacklists and string filtering fail to stop LFI?
Is this vulnerability specific to PHP?
require(), Python's dynamic imports, and Java classloaders, though PHP's include()/require() combined with stream wrappers makes the exploitation path unusually direct.