WP REST Cache <= 2026.1.0 - Unauthenticated Stored Cross-Site Scripting
Description
The WP REST Cache plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 2026.1.0 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=2026.1.0What Changed in the Fix
Changes introduced in v2026.1.1
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-25347 ## 1. Vulnerability Summary The **WP REST Cache** plugin (up to version 2026.1.0) is vulnerable to **Unauthenticated Stored Cross-Site Scripting (XSS)**. The plugin caches REST API requests and displays metadata about these caches (such as Request URI an…
Show full research plan
Exploitation Research Plan: CVE-2026-25347
1. Vulnerability Summary
The WP REST Cache plugin (up to version 2026.1.0) is vulnerable to Unauthenticated Stored Cross-Site Scripting (XSS). The plugin caches REST API requests and displays metadata about these caches (such as Request URI and Request Headers) in the WordPress admin dashboard. The vulnerability exists because the plugin fails to sanitize or escape this metadata when rendering the "Caches" list table and specific cache detail views.
An unauthenticated attacker can inject a malicious script into a REST API request (e.g., via query parameters). The plugin caches this request, storing the payload in the database. When an administrator views the plugin's settings or cache logs, the script executes in their browser.
2. Attack Vector Analysis
- Vulnerable Endpoint: Any REST API endpoint cached by the plugin (default WordPress endpoints like
/wp-json/wp/v2/postsare targeted by default). - Vulnerable Parameter: The Request URI (specifically query strings) or Request Headers.
- Authentication: None (Unauthenticated).
- Preconditions:
- The plugin must be active.
- The targeted REST endpoint must be allowed for caching (the plugin allows default WP endpoints out of the box).
- A "Must-Use" plugin (
wp-rest-cache.php) is typically required for the caching mechanism to trigger (automatically installed/copied by the plugin).
3. Code Flow
Storage (Injection):
- An unauthenticated user sends a GET request to a cached endpoint:
/wp-json/wp/v2/posts?debug=<script>alert(1)</script>. - The plugin (via the MU-plugin or early hooks) intercepts the request.
WP_Rest_Cache_Plugin\Includes\Caching\Caching::set_cacheis called.- The URI (including the XSS payload) is passed to
register_endpoint_cache. - The data is stored in the
{prefix}wrc_cachestable. Therequest_uricolumn now contains the raw payload.
- An unauthenticated user sends a GET request to a cached endpoint:
Display (Sink):
- An administrator navigates to Settings > WP REST Cache.
- The page initializes the
WP_Rest_Cache_Plugin\Admin\Includes\API_Caches_Tableclass (derived fromWP_List_Table). - In
admin/includes/class-api-caches-table.php, thecolumn_defaultmethod (line 198) is used to render columns likerequest_uri.
public function column_default( $item, $column_name ): string { return $item[ $column_name ]; // RAW OUTPUT SINK }- Additionally, the
column_cache_keymethod (line 119) renders thecache_keywithout escaping the anchor text:
$title = sprintf( '<strong><a href="?page=%s&sub=%s&cache_key=%s">%s</a></strong>', esc_attr( $page ), 'cache-details', esc_attr( $item['cache_key'] ), $item['cache_key'] // RAW OUTPUT SINK );
4. Nonce Acquisition Strategy
This exploit is unauthenticated and does not require a nonce for the injection phase. The payload is "injected" simply by making a standard GET request to the WordPress REST API.
If the automated agent needs to verify the XSS by performing an action (like clearing the cache) via the admin UI, it would find the nonce in the admin page HTML.
- Admin Page:
/wp-admin/options-general.php?page=wp-rest-cache - Nonce Localized Key: The plugin does not appear to use
wp_localize_scriptfor these specific nonces in the provided source; they are generated inline in the table columns:wp_create_nonce( 'wp_rest_cache_flush_cache' )wp_create_nonce( 'wp_rest_cache_delete_cache' )
Note: For the purpose of proving Stored XSS, no nonce is needed; the goal is to store the payload and observe its execution upon an admin's page load.
5. Exploitation Strategy
The exploitation involves sending a request that the plugin will log.
Injection Request:
- Method:
GET - URL:
/wp-json/wp/v2/posts?id=1&xss_param=<img src=x onerror=alert(document.domain)> - Headers: None required.
- Expected Behavior: The REST API responds normally. The plugin detects the GET request to a cacheable endpoint and records the URI in the database.
- Method:
Triggering the XSS:
- Log into WordPress as an Administrator.
- Navigate to
http://localhost:8080/wp-admin/options-general.php?page=wp-rest-cache. - The browser will render the
WP_List_Table. The row for the injected request will contain the payload in the "Request URI" column, triggering thealert.
6. Test Data Setup
- Plugin Activation: Install and activate
wp-rest-cacheversion 2026.1.0. - MU-Plugin Verification: Ensure
wp-content/mu-plugins/wp-rest-cache.phpexists (this is how the plugin hooks into early requests). - Content: Ensure at least one post exists so that
/wp-json/wp/v2/postsreturns a 200 OK (though the plugin may cache 404s/empty sets depending on configuration).
7. Expected Results
- The injection request should result in a new entry in the
wrc_cachestable. - When the admin page loads, the HTML source for the table will contain the literal string
<img src=x onerror=alert(document.domain)>within a<td>tag. - The
alert(document.domain)will fire in the admin's browser session.
8. Verification Steps
- DB Check: Use WP-CLI to verify the payload is stored:
wp db query "SELECT request_uri FROM wp_wrc_caches WHERE request_uri LIKE '%onerror%'" - HTTP Check: Use the
http_requesttool to fetch the admin page and verify the unescaped payload exists in the response body:# (Simplified representation) GET /wp-admin/options-general.php?page=wp-rest-cache # Check for: <td>/wp-json/wp/v2/posts?id=1&xss_param=<img src=x onerror=alert(document.domain)></td>
9. Alternative Approaches
If request_uri is filtered by the web server or WordPress core before the plugin stores it:
- Header Injection: Inject via a header that the plugin is configured to cache (e.g., if a developer adds a custom header to
wp_rest_cache_global_cacheable_request_headers).- Request:
GET /wp-json/wp/v2/postswith HeaderX-Custom-Header: <script>alert(1)</script>.
- Request:
- Cache Key Injection: The
cache_keyitself is rendered raw incolumn_cache_key. If an attacker can influence the generation of the cache key (often a hash, but sometimes includes components of the URI), this is a viable sink. - Details Page: Check the specific "Cache Details" page:
?page=wp-rest-cache&sub=cache-details&cache_key=[KEY]. Althoughsub-cache-details.phpusesesc_htmlin most places, any overlooked metadata column would result in XSS.
Summary
The WP REST Cache plugin for WordPress stores REST API request metadata, such as URIs and headers, in its database to facilitate cache management. Versions up to 2026.1.0 are vulnerable to unauthenticated stored XSS because the plugin fails to sanitize or escape these values when rendering the cache list table in the admin dashboard, allowing an attacker to execute arbitrary scripts in an administrator's browser session.
Vulnerable Code
// admin/includes/class-api-caches-table.php:120 $title = sprintf( '<strong><a href="?page=%s&sub=%s&cache_key=%s">%s</a></strong>', esc_attr( $page ), 'cache-details', esc_attr( $item['cache_key'] ), $item['cache_key'] ); --- // admin/includes/class-api-caches-table.php:198 public function column_default( $item, $column_name ): string { return $item[ $column_name ]; }
Security Fix
@@ -127,7 +127,7 @@ esc_attr( $page ), 'cache-details', esc_attr( $item['cache_key'] ), - $item['cache_key'] + esc_html( $item['cache_key'] ) ); $actions = []; @@ -196,7 +196,7 @@ * @return string The output for this column. */ public function column_default( $item, $column_name ): string { - return $item[ $column_name ]; + return esc_html( $item[ $column_name ] ); }
Exploit Outline
The exploit is achieved by sending a standard GET request to any REST API endpoint that the plugin is configured to cache (such as default WordPress endpoints like /wp-json/wp/v2/posts). The attacker appends a malicious XSS payload as a query parameter (e.g., /wp-json/wp/v2/posts?debug=<script>alert(1)</script>). Because the plugin intercepts and logs the full Request URI into the database, the payload is stored. When an administrator later views the plugin's 'Caches' table at /wp-admin/options-general.php?page=wp-rest-cache, the unescaped URI is rendered in the 'Request URI' column, triggering the stored script. No authentication or nonces are required for the initial injection.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.