The Exploit
An authenticated Contributor-level user with access to the [code_snippet] shortcode can inject arbitrary PHP via the shortcode attributes parameter when "Enable file-based execution" is active and at least one snippet is enabled.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in=<valid_contributor_session>
action=code_snippet&atts[filepath]=/tmp/shell.php&atts[snippet]=system('id > /tmp/pwned.txt')
The server executes the injected filepath value and any PHP within the snippet attribute. An attacker observes either direct code execution output in the response or—more reliably—file system side effects like the creation of /tmp/pwned.txt containing command output. No administrator interaction is required beyond the initial setup of the plugin with file-based execution enabled.
Why this still matters at Contributor level: Contributor-level access is a low-privilege role intended only for draft submission; in production WordPress installs, it is routinely granted to external writers, contractors, and automated publishing systems. A compromised Contributor session (via credential theft, session fixation, or phishing) becomes a direct RCE vector.
What the Patch Did
Before
extract( $atts );
After
extract( $atts, EXTR_SKIP );
The patch adds the EXTR_SKIP flag to the extract() function call. By default, extract() operates in EXTR_OVERWRITE mode, which allows any key in the input array to overwrite variables already present in the function scope. The EXTR_SKIP flag reverses this logic: only keys that do not already correspond to existing variables are extracted. This prevents attacker-controlled shortcode attributes from overwriting critical variables like $filepath that are later passed to require_once() or used in other dangerous contexts.
Root Cause
CWE-1104: Use of Unmaintained Third-Party Components (though the underlying issue is CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code).
The vulnerability exists in php/front-end/class-front-end.php in the evaluate_shortcode_from_flat_file() method. Shortcode attributes are parsed into the $atts array by WordPress's shortcode parser and passed directly to extract() without restriction. Because extract() defaults to EXTR_OVERWRITE, any key matching a variable name in scope—including $filepath—overwrites the original value. The attacker-controlled filepath attribute then replaces the intended snippet path, and subsequent require_once($filepath) calls execute arbitrary code from the attacker-specified location. The dataflow is:
$_REQUEST['atts'] → shortcode_atts() → $atts array → extract($atts) → overwritten $filepath → require_once($filepath).
Why It Works
The single load-bearing line is the flag EXTR_SKIP. Without it, extract() will happily overwrite $filepath with whatever an attacker supplies in the shortcode. With it, $filepath is already bound in the function scope before extract() runs, so the attacker-supplied value is silently ignored—the original $filepath is never mutated.
Why did the engineer not simply remove extract() entirely and use isset() checks instead? That would be the stronger fix, but extract() is still found throughout WordPress core and plugins because it is convenient for unpacking template variables. The EXTR_SKIP approach is a pragmatic middle ground: it preserves the convenience of extract() while blocking the most dangerous mutation patterns. The fix is minimal, reviewable, and consistent with WordPress security recommendations in the Plugin Developer Handbook.
Hardening Checklist
-
Do not use
extract()on untrusted input: Replaceextract( $atts )with explicit assignment ($var = isset($atts['var']) ? sanitize_text_field($atts['var']) : default_value) or usewp_parse_args()with default values. -
Use
wp_kses_post()orsanitize_text_field()on all shortcode attributes: Even withEXTR_SKIP, attributes should be sanitized before use. Do not assume that attribute keys are safe. -
Restrict file-based code execution with capability checks: Wrap the
evaluate_shortcode_from_flat_file()call incurrent_user_can('manage_options')to limit execution to administrators only. A Contributor-level user should never be able to control filepath parameters. -
Use
realpath()and whitelist validation on$filepath: Resolve the path and verify it is within an expected directory. Reject any path containing..or pointing outside the plugin's snippet directory. -
Audit all uses of
require(),include(), andeval(): These are automatic RCE sinks. Immediately flag any where the path or code derives from user input, even transitively.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-13035
- Code Snippets Plugin Repository: https://wordpress.org/plugins/code-snippets/