The Exploit
An authenticated contributor can traverse the filesystem and execute arbitrary PHP by uploading an SVG file and including it via a path traversal payload in the get_svg() function.
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=elementor_render_widget&post_id=1&widget_id=inline_svg_widget_1&settings={"url":"../../wp-content/uploads/2025/01/shell.svg"}
The attacker observes the SVG file contents reflected in the rendered widget output, or alternatively sees PHP code execution if the uploaded SVG payload contains valid PHP wrapped in script tags or embedded in SVG metadata that the server processes. A malicious SVG uploaded to /wp-content/uploads/ becomes executable when included via the traversal path.
What the Patch Did
Before
return Plugin::$plugin_path . 'assets/img/' . $file_name . '.svg';
Before (second location)
$svg = file_get_contents( $svg['url'] ); //phpcs:ignore
$classes = [ 'svg-wrapper' ];
After
$file_name = basename( $file_name );
return Plugin::$plugin_path . 'assets/img/' . $file_name . '.svg';
After (second location)
if ( ! filter_var( $svg['url'], FILTER_VALIDATE_URL ) || pathinfo( $svg['url'], PATHINFO_EXTENSION ) !== 'svg' ) {
return;
}
$svg = file_get_contents( $svg['url'] ); //phpcs:ignore
$classes = [ 'svg-wrapper' ];
The patch added two distinct security controls. In utils.php, it introduced basename() to strip directory traversal sequences from the filename before path concatenation. In the widget, it added URL format validation via filter_var() with FILTER_VALIDATE_URL and file extension whitelisting via pathinfo(), preventing file_get_contents() from loading non-SVG or maliciously-crafted URLs. Together, these controls collapse two separate LFI attack vectors that converge at the same execution sink.
Root Cause
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') combined with CWE-20: Improper Input Validation.
The vulnerability flows through two chained locations. First, a user-controlled SVG filename or URL parameter ($file_name or $svg['url']) enters the request without sanitization. In utils.php, the filename is directly concatenated into a filesystem path: Plugin::$plugin_path . 'assets/img/' . $file_name . '.svg'. An attacker supplies $file_name = '../../wp-content/uploads/2025/01/shell', and file_get_contents() loads /wp-content/uploads/2025/01/shell.svg instead of the intended /wp-content/plugins/jupiter-x-core/assets/img/shell.svg. The second vector in the widget accepts a URL directly into file_get_contents() with no validation, allowing file:// protocols or relative paths. Both cross the trust boundary from user input to filesystem access without a confinement check.
Why It Works
The load-bearing line is $file_name = basename( $file_name );. The basename() function in PHP strips all directory path components, leaving only the trailing filename. If you remove it, payloads like ../../wp-content/uploads/shell still traverse directories unchecked. The second location adds two guards: filter_var() rejects malformed URLs and non-URL protocols (like file://), while pathinfo() ensures the extension is .svg. Neither alone is sufficient — removing the extension check allows .php files; removing the URL filter allows filesystem protocols. The engineer added both because one defends the shape of input (is it a valid URL at all?) and the other defends the type of file (is it an SVG?). Defense-in-depth here means the attacker must forge both a syntactically valid URL and trick the extension check, raising the bar significantly over a single validation layer.
Hardening Checklist
- Use
wp_safe_remote_get()instead offile_get_contents()for remote resources and applywp_kses_post()to SVG content before rendering to mitigate both SSRF and XSS. - Constrain all file operations to a whitelist directory using
realpath()and string prefix comparison — verify thatrealpath( $resolved_path )begins withrealpath( $allowed_base_dir )before any read operation. - Apply
sanitize_file_name()to all user-supplied filenames before filesystem operations, even when usingbasename(), to strip null bytes and other problematic characters that bypass simple path traversal checks. - Validate file extensions against a strict whitelist using
wp_check_filetype()rather than string manipulation, which respects WordPress's own MIME type registry. - Audit all calls to
file_get_contents(),include,require, andfopen()in your codebase and trace the origin of the path or URL argument — if it touches user input in any form, apply validation before the sink, not after.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-0366