The Exploit
A contributor-level WordPress user can include arbitrary PHP files from the server filesystem by sending a crafted request to the ElementsKit plugin's Onepage Scroll module.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target-wordpress.local
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in=<contributor_session>
action=elementkit_onepage_scroll_nav_markup&nav_style=../../../../etc/passwd%00&nonce=<valid_nonce>
When this request executes, the nav_style parameter bypasses all validation and reaches the include_once statement in extend-controls.php line 186. The attacker observes either the contents of /etc/passwd rendered in the page response, or — more usefully — a poisoned file they uploaded to the WordPress media library (e.g., ../../wp-content/uploads/2024/malicious.php) executes on the server. The attacker achieves arbitrary PHP code execution with the privileges of the web server process.
What the Patch Did
Before
include_once 'nav-styles/' . $nav_style . '.php';
After
$nav_styles = array(
'circle-scale-up',
'circle-fill-in',
'circle-fill-out',
'circle-stroke',
'circle-stroke-dot',
'circle-stroke-simple',
'circle-dot-move',
'circle-timeline',
'square-scale-up',
'line-grow',
'line-shrink',
'line-fill',
'line-move',
'icon',
);
if( in_array($nav_style, $nav_styles) ) {
include_once 'nav-styles/' . $nav_style . '.php';
}
The patch implements a whitelist validation strategy using PHP's in_array() function. The $nav_style variable is compared against a hardcoded array of fourteen legitimate navigation style identifiers. The include_once statement only executes if the user-supplied value matches an entry in that whitelist. This is a negative-space defense: rather than attempting to filter dangerous patterns (which is error-prone), the code now accepts only known-safe values. Any attempt to inject path traversal sequences like ../, null bytes, or arbitrary filenames fails the in_array() check and prevents file inclusion entirely.
Root Cause
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
The nav_style parameter arrives from an AJAX POST request to the elementkit_onepage_scroll_nav_markup action. The ElementsKit plugin extracts this user-controlled value without sanitization or validation and concatenates it directly into a filesystem path: 'nav-styles/' . $nav_style . '.php'. This path is passed to include_once, which interprets ../ sequences as directory traversal operators. An authenticated contributor can weaponize this dataflow to escape the intended nav-styles/ directory and include files from anywhere on the filesystem readable by the web server process — including uploaded media, configuration files, or system files. The trust boundary between user input and filesystem access is crossed without any security control.
Why It Works
The load-bearing line is the in_array($nav_style, $nav_styles) check. Remove it, and the vulnerability immediately reopens: the concatenation and include_once operate identically to the original code. The whitelist itself — the fourteen hardcoded style names — is the actual control. The engineer added the array declaration and the if wrapper to enforce this control at the point of greatest risk: immediately before the file operation. This is classic allowlist-based path confinement. A secondary strength of the patch is that it uses a static, inline array rather than a dynamic configuration or database lookup, eliminating the possibility of an attacker manipulating the whitelist at runtime. The approach assumes that navigation styles are a fixed, unchanging feature set — a reasonable assumption for a UI component library.
Hardening Checklist
-
Use
wp_safe_remote_get()andwp_remote_retrieve_body()for file reads instead ofinclude/require— If the included file is only meant to be data, read it as a string andeval()it only after validation, or better yet, use templating. Direct inclusion of user-influenced paths should be rare. -
Implement
realpath()confinement before any file operation — Resolve the final path to its canonical form and verify it lies within an expected directory usingstrpos(realpath($path), realpath($allowed_dir)) === 0. This catches symlink attacks and.or..sequences PHP's path resolution might otherwise permit. -
Apply
sanitize_file_name()to any user input destined for a filesystem operation — WordPress's function removes path separators and traversal sequences. Pair it within_array()or a regex whitelist for defense-in-depth. -
Audit all
include,require,file_get_contents(), andfopen()calls in plugin code for user-controlled input — Use a static analysis tool or grep for these functions with$_POST,$_GET,$_REQUEST, or any variable derived from them without intervening validation. -
Store included files outside the web root or in a directory with restrictive file permissions — If a path traversal bug slips past code review, it still cannot include or execute a file the web server user cannot read. This is a defense-in-depth layer.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-3499