How a Local File Inclusion Bug Becomes Remote Code Execution Without an Upload
File inclusion only lets an attacker pick which local file runs. This explains the gadget that turns that primitive into attacker-controlled code, and why configuration decides exploitability.
When an advisory says a file-inclusion bug "may lead to remote code execution," the jump from reading a file to running attacker code is doing a lot of quiet work. The WordPress core flaw CVE-2026-87902, patched in September 2026, is a clean case study: the bug itself only lets an attacker choose which local PHP file gets included, yet in the wild it produced full code execution. Understanding how that escalation works tells you which of your systems are actually at risk and which are merely running vulnerable code.
What Local File Inclusion Actually Does
Local file inclusion, or LFI, is a bug where an application uses attacker-influenced input to decide which file to load from the local filesystem. In a language like PHP, "load" often means `include` or `require`, and those statements do not just read a file, they execute any PHP inside it. That is the crucial distinction from a plain file read. A path-traversal bug that leaks a file's contents is serious, and the mechanics of escaping the intended directory are covered in How Path TraversalPath Traversal🛡️A web vulnerability (CWE-22) where user-supplied input in a file path escapes the directory the application intended to serve from, typically via parent-directory references, letting an attacker read or write files elsewhere on the server. Bugs Let Attackers Read Files Outside the Web Root. But inclusion turns that same escape into an execution primitive, because the target file is not returned to the attacker, it is run by the server.
By itself, that primitive seems limited. An attacker can only include files that already exist on disk. If every PHP file on the server is legitimate application code, including one of them just runs the application in a slightly wrong order. The danger appears when the attacker can point the inclusion at a file whose behavior they can control, or at a file they managed to write earlier.
The Gadget Problem
ExploitExploit🛡️Code or technique that takes advantage of a vulnerability to cause unintended behavior, such as gaining unauthorized access. developers call a locally present file that converts a limited primitive into full execution a gadget. For LFI-to-RCE, a gadget is any readable file that, when included, does something the attacker can steer through the same request.
The classic gadget is PEAR's `pearcmd.php`, a command-line helper that ships with many PHP installations. When the PHP directive `register_argc_argvregister_argc_argv🛡️A PHP configuration directive, enabled by default, that populates the argument variables from request data. Web applications rarely need it, but leaving it on lets gadgets such as PEAR's command helper read attacker-controlled arguments from the query string, a key link in local-file-inclusion attack chains.` is enabled, `pearcmd.php` reads its "command arguments" from the request query string. An attacker who can include that file can therefore hand it commands, most usefully one that writes a chosen string to a chosen path. The result is that the attacker writes a small PHP file into a world-writable directory such as a temporary folder, then uses the same inclusion bug to include that new file. Now the server is running code the attacker authored moments earlier. This is exactly the chain observed against CVE-2026-87902, where honeypots caught requests probing for `pearcmd` and then attempting to create PHP files under temporary directories.
Gadgets are not limited to PEAR. Session files, uploaded avatars with PHP smuggled inside, and log files poisoned with PHP through an earlier request have all served the same role in different applications. The common threadThread🏠A low-power mesh networking protocol designed for IoT devices, used alongside Matter. is that the attacker finds some way to get controlled bytes onto disk in a readable location, then uses the inclusion bug to execute them. The gadget is what carries the payload; the inclusion bug is only the trigger.
Why Configuration Decides Exploitability
Because RCE depends on a gadget, two servers running identical vulnerable code can have very different risk. If `register_argc_argv` is disabled, the `pearcmd` gadget stops working. If PEAR is not installed, the file is not there to include. If the runtime confines PHP to a directory tree, the inclusion cannot reach `/tmp` or the PEAR path at all. This is why the WordPress advisory framed exploitation as conditional rather than universal, and why the same bug is critical on a default Docker-based host but inert on a locked-down one.
That gap between "vulnerable" and "exploitable" is the whole reason hardening the runtime matters. A file-inclusion bug that lands on a host with no usable gadget and no writable execution path produces an error, not a shell. Building that condition deliberately, rather than relying on luck, is the subject of How to Harden PHP So a File-Inclusion Bug Cannot Reach Code Execution.
What This Means for Defenders
Three practical points follow. First, treat file-inclusion and file-read bugs as potential code execution until you have confirmed no gadget path exists on the affected host; do not downgrade the severity in your own head just because the advisory only proves inclusion. Second, when triaging, inventory the gadget preconditions directly, whether PEAR is present, whether `register_argc_argv` is on, whether the PHP process can write to a directory it can also include from. Those answers move a finding between "patch this quarter" and "patch tonight." Third, remember that patching the application closes the specific trigger but leaves the gadget in place for the next inclusion bug; the runtime hardening is what protects you against the bug you have not read an advisory for yet.
CVE-2026-87902 is a reminder that the interesting question is rarely whether an application has an inclusion bug. Most large codebases eventually ship one. The question is whether your environment turns that bug into a foothold, and that is a decision you make in your runtime configuration long before any advisory lands. The strategic side of getting the fix deployed quickly once it does land is covered in Why Auto-Updates Are Not a Substitute for a Patch Response Plan.