In short: Local file inclusion in PHP arises when include, require or file functions use a path that the visitor can influence. The safe solution is a fixed allow-list: the parameter only selects from known values, and the path itself never comes from the request. On the server side, allow_url_include=Off, open_basedir and restrictive file permissions complement the protection.
What is local file inclusion in PHP?
Many older PHP applications load pages by a simple pattern: a parameter in the address determines which file is included. If this parameter is passed unchecked to include, an attacker can request other files on the server. In the milder case, they read configuration files with credentials. In the worse case, they get PHP to execute content they placed elsewhere beforehand, for example in an upload or a log file.
For management, the key point is: this vulnerability rarely sits in the CMS core, but mostly in custom developments, old extensions or quickly built add-on pages. It goes unnoticed in everyday operation because the website works normally.
With remote file inclusion, PHP loads code from a foreign address. This requires allow_url_include to be enabled. In current PHP versions this setting is off by default, but on old or wrongly configured servers it can still be found.
Typical vulnerable patterns in code
At risk are places where a value from the request, from cookies or from the database becomes part of a file path. Common examples are language switchers that load a language file according to a parameter, page selection via a parameter in the address, template or theme selection, and download scripts for documents. An appended .php at the end of the path does not protect reliably, because attackers can switch to existing PHP files.
You get a first overview with a search for inclusions that contain variables. It also returns harmless hits but shows where to look more closely:
grep -rnE --include='*.php' '(include|require)(_once)?\s*\(?\s*\$' /var/www/projekt \
| grep -v '/vendor/'
Check each hit for where the variable comes from and whether a visitor can influence it.
The safe pattern: allow-list instead of a path from the request
The most reliable protection is that the visitor selects only a key and the application itself decides which file belongs to it. The path is never assembled from input.
<?php
declare(strict_types=1);
$seiten = [
'start' => __DIR__ . '/../vorlagen/start.php',
'leistungen' => __DIR__ . '/../vorlagen/leistungen.php',
'kontakt' => __DIR__ . '/../vorlagen/kontakt.php',
];
$wahl = is_string($_GET['seite'] ?? null) ? $_GET['seite'] : 'start';
if (!array_key_exists($wahl, $seiten)) {
http_response_code(404);
$wahl = 'start';
}
require $seiten[$wahl];
This pattern is short, easy to check and leaves no room for manoeuvre. In modern frameworks the router takes over this task; there too, controllers and templates should never be determined from free parameters.
When file names have to be dynamic: realpath and base directory
Sometimes a fixed list is not practical, for example with downloads from a documents folder. In that case, check the resolved path against a fixed base directory and allow only simple file names.
<?php
declare(strict_types=1);
const BASIS = '/var/www/daten/dokumente';
function sichererPfad(string $name): string
{
if (!preg_match('/^[a-z0-9_-]{1,64}\.pdf$/', $name)) {
throw new InvalidArgumentException('Ungültiger Dateiname.');
}
$basis = realpath(BASIS);
$pfad = realpath($basis . DIRECTORY_SEPARATOR . $name);
if ($basis === false || $pfad === false
|| !str_starts_with($pfad, $basis . DIRECTORY_SEPARATOR)) {
throw new RuntimeException('Datei nicht gefunden.');
}
return $pfad;
}
// Deliver instead of include: readfile() does not execute code.
$datei = sichererPfad((string) ($_GET['datei'] ?? ''));
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="' . basename($datei) . '"');
readfile($datei);
Two points are decisive. The regular expression allows no directory separators and no special characters. And files that are only to be delivered are read with readfile(), not with include, so that contained code is never executed. str_starts_with() is available from PHP 8.0.
Server-side hardening: php.ini, open_basedir and permissions
Secure code is the first line of defence. The server configuration ensures that an overlooked mistake does less damage.
; php.ini or FPM pool (php_admin_value / php_admin_flag)
allow_url_include = Off
allow_url_fopen = Off ; only if the application does not load external URLs
open_basedir = /var/www/kunde-a/:/tmp/kunde-a/
display_errors = Off
log_errors = On
Some applications need allow_url_fopen for updates or interfaces. Test switching it off in a test environment first. open_basedir limits PHP to the website's own directories, which is particularly important on servers with several customers. Details are in the guides open_basedir and isolation per website and Configuring php.ini securely.
In addition, upload and cache directories should not be executable and log files should not lie in the web root. This leaves an attacker without places where they could put their own content for inclusion.
How to recognise exploitation attempts in logs
Attempts to exploit a file inclusion usually leave traces in the access log. Conspicuous are parameters containing directory traversal, absolute system paths, stream wrappers or complete foreign addresses, especially in parameters such as page, file, template or lang. A single hit is usually automated scanning. What matters is whether such a request was answered with status code 200 and an unusual response size.
How to proceed systematically is described in Analysing web server logs after an attack. If you find signs of successful access, assume that configuration files were read, and renew the credentials they contain.
These are above all database passwords, keys for payment and shipping services, SMTP credentials and the CMS's secret keys. Also check whether new files have appeared since the first suspicious access. Anyone who could read files may also have tried to place their own. In this case it is no longer only a vulnerability in the code but a security incident that should be handled in a structured way.
Securing fixes permanently
After a fix, it is worth backing up the protective function with automated tests. A simple test passes the page selection unknown values, directory traversal and empty input and expects the default page or a 404 error in every case. Such tests prevent a later change from silently reopening the vulnerability.
For management and project leads, one question to the agency is helpful: are there places in the application where files are loaded based on parameters, and how are they secured? A concrete answer with locations and tests is a good sign.
How does PHP SOS help?
- We search your code specifically for
include,requireand file functions with input from the request and assess every finding. - We set
allow_url_include,open_basedirand suitable permissions per website and check that the application keeps working afterwards. - We analyse logs for exploitation attempts and clarify whether credentials need to be renewed.
- For an independent review, we offer a security audit with penetration test for PHP applications.
Frequently asked questions
What is the difference between LFI and RFI?
With local file inclusion, the application loads a file that is already on the server but should not be loaded. With remote file inclusion, code is loaded from a foreign address. RFI requires an unsafe PHP setting; LFI does not.
Is it enough to remove ../ from the parameter?
No. Such filters can often be bypassed with encoding or variants and overlook absolute paths. Only a fixed mapping of permitted values to files, or a check of the resolved path against a base directory, is safe.
Does open_basedir protect completely against file inclusion?
No, it only limits the directories PHP may access. Within those limits a vulnerability remains exploitable. open_basedir is an additional layer of protection and does not replace secure code.
Are CMS such as WordPress affected by LFI?
The core of common CMS is regarded as well reviewed on this point. Vulnerabilities appear more often in extensions, themes and custom developments. Updates of the extensions and a look at their security advisories are therefore important.