Skip to content

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.

Updated 02 Nov 2026Checked against cli@de9b52a
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

  1. 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';
  1. 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;
  1. Disable allow_url_include and allow_url_fopen in php.ini. These should always be Off in 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
  1. 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

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.

Edit this pageSources: cli/internal/sast/rules/vnx-98.rego, cli/internal/sast/docs/vnx-98.md
esc
↑ ↓ move↵ openSearches in your browser. A key like autonomy.max or a code like threshold_required jumps straight to it.