CVE-2026-25347

WP REST Cache <= 2026.1.0 - Unauthenticated Stored Cross-Site Scripting

highImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
7.2
CVSS Score
7.2
CVSS Score
high
Severity
2026.1.1
Patched in
4d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=2026.1.0
PublishedMarch 23, 2026
Last updatedMarch 26, 2026
Affected pluginwp-rest-cache

What Changed in the Fix

Changes introduced in v2026.1.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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/posts are targeted by default).
  • Vulnerable Parameter: The Request URI (specifically query strings) or Request Headers.
  • Authentication: None (Unauthenticated).
  • Preconditions:
    1. The plugin must be active.
    2. The targeted REST endpoint must be allowed for caching (the plugin allows default WP endpoints out of the box).
    3. 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

  1. 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_cache is called.
    • The URI (including the XSS payload) is passed to register_endpoint_cache.
    • The data is stored in the {prefix}wrc_caches table. The request_uri column now contains the raw payload.
  2. Display (Sink):

    • An administrator navigates to Settings > WP REST Cache.
    • The page initializes the WP_Rest_Cache_Plugin\Admin\Includes\API_Caches_Table class (derived from WP_List_Table).
    • In admin/includes/class-api-caches-table.php, the column_default method (line 198) is used to render columns like request_uri.
    public function column_default( $item, $column_name ): string {
        return $item[ $column_name ]; // RAW OUTPUT SINK
    }
    
    • Additionally, the column_cache_key method (line 119) renders the cache_key without 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_script for 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.

  1. 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.
  2. 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 the alert.

6. Test Data Setup

  1. Plugin Activation: Install and activate wp-rest-cache version 2026.1.0.
  2. MU-Plugin Verification: Ensure wp-content/mu-plugins/wp-rest-cache.php exists (this is how the plugin hooks into early requests).
  3. Content: Ensure at least one post exists so that /wp-json/wp/v2/posts returns 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_caches table.
  • 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

  1. 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%'"
    
  2. HTTP Check: Use the http_request tool 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:

  1. 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/posts with Header X-Custom-Header: <script>alert(1)</script>.
  2. Cache Key Injection: The cache_key itself is rendered raw in column_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.
  3. Details Page: Check the specific "Cache Details" page: ?page=wp-rest-cache&sub=cache-details&cache_key=[KEY]. Although sub-cache-details.php uses esc_html in most places, any overlooked metadata column would result in XSS.
Research Findings
Static analysis — not yet PoC-verified

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

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-rest-cache/2026.1.0/admin/includes/class-api-caches-table.php /home/deploy/wp-safety.org/data/plugin-versions/wp-rest-cache/2026.1.1/admin/includes/class-api-caches-table.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-rest-cache/2026.1.0/admin/includes/class-api-caches-table.php	2024-03-07 20:02:54.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-rest-cache/2026.1.1/admin/includes/class-api-caches-table.php	2026-01-28 07:48:12.000000000 +0000
@@ -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.