VNX-98: PHP Remote File Inclusion
PHP include/require statements whose path carries request data (superglobals, php://input, filter_input, $request->input), inline or through a variable, with no basename(), cast or allowlist check.
| Field | Value |
|---|---|
| Rule ID | VNX-98 |
| Severity | Critical |
| SARIF level | error |
| Kind | sast |
| Languages | PHP |
| CWE | CWE-98 |
| CAPEC | CAPEC-253 |
| ATT&CK | T1059.004 |
| CWSS vector | CWSS:1.0/TI:C/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:C/DI:H/EX:H/EC:N/P:W |
| Tags | rfi file-inclusion php cwe-98 |
| Source | internal/sast/rules/vnx-98.rego |
Overview
This rule flags PHP include, include_once, require, and require_once statements whose path carries request data ($_GET, $_POST, $_REQUEST, $_COOKIE, $_FILES, php://input, filter_input(), Laravel/Symfony $request->input()), inline, interpolated, or through a variable assigned from it in the 25 lines above. A path passed through basename() or an integer cast, or checked against an allowlist (in_array, array_key_exists, preg_match) before the include, is not reported. 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’).
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: request data inline or interpolated in the include path
include($_GET['module'] . '.php');
require $_POST['lib'];
include "pages/{$_GET['p']}.php";
// FLAGGED: through a variable, even after a str_replace() blacklist
$file = $_GET['file'];
$file = str_replace(array("../", "..\\"), "", $file);
include($file);
// FLAGGED: Laravel request accessor
$theme = $request->input('theme');
include resource_path('themes/' . $theme . '/layout.php');
Not flagged: require_once "lang/" . basename($_GET['lang']) . ".php";, a $page checked with in_array($page, $allowed, true) before the include, require_once __DIR__ . '/vendor/autoload.php';, and variables not read from the request in the same file.
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.
References
- CWE-98: PHP Remote File Inclusion
- OWASP File Inclusion
- OWASP PHP Security Cheat Sheet
- CAPEC-253: Remote Code Inclusion
- MITRE ATT&CK T1059.004 – Unix Shell
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-98
// nosec vnx-98
Or record an ignore rule: vulnetix ignore add --rule vnx-98 --reason "...". See Suppressing findings.