CVE-2025-14797

Same Category Posts <= 1.1.19 - Authenticated (Author+) Stored Cross-Site Scripting via Widget Title Placeholder

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

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

Technical Details

Affected versions<=1.1.19
PublishedJanuary 23, 2026
Last updatedJanuary 24, 2026
Affected pluginsame-category-posts

Source Code

WordPress.org SVN
Research Plan
Unverified

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 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 SameCategoryPostsWidget class (likely in same-category-posts.php or 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 through htmlspecialchars_decode(). This allows any HTML entities stored in a category or tag name (like &lt;script&gt;) 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:
    1. The "Same Category Posts" widget must be active in a sidebar.
    2. The widget title must contain a supported placeholder (e.g., Posts in %category%).

3. Code Flow (Inferred from Patch Description)

  1. Entry Point: A user views a single post on the frontend.
  2. Widget Execution: WordPress calls SameCategoryPostsWidget::widget($args, $instance).
  3. Data Retrieval: The plugin identifies the current post's terms (categories/tags) using get_the_category() or get_the_terms().
  4. 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 &lt;script&gt;alert(1)&lt;/script&gt;.
    • It performs a replacement: $title = str_replace('%category%', $term_name, $title);.
    • The Sink: The plugin calls echo htmlspecialchars_decode($title);.
  5. 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 POST requests to edit-tags.php or post.php.
  • The agent should use browser_navigate to wp-admin/edit-tags.php?taxonomy=category and then use browser_eval to extract the _wpnonce from the form.
  • JavaScript Variable: document.querySelector('#addtag input[name="_wpnonce"]').value.

5. Test Data Setup

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

  1. Step 1: Login as Author
    • Use http_request to authenticate as the Author.
  2. Step 2: Create Malicious Category
    • Endpoint: POST /wp-admin/edit-tags.php
    • Payload:
      • action: add-tag
      • screen: edit-category
      • taxonomy: category
      • tag-name: <img src=x onerror=alert("CVE-2025-14797")>
      • slug: xss-cat
      • _wpnonce: (Extracted via browser_eval)
  3. Step 3: Create Post in Category
    • Create a new post and assign it to the xss-cat category.
    • Endpoint: POST /wp-admin/post.php
    • Parameters: post_type=post, post_category[]=[ID_OF_XSS_CAT], post_title=XSS Trigger Post, publish=Publish.
  4. Step 4: Trigger XSS
    • Use http_request (GET) to navigate to the permalink of the newly created post.
    • Examine the HTML response.

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: &lt;img src=x onerror=...&gt;. The call to htmlspecialchars_decode() converts it back to active HTML.

8. Verification Steps

  1. Manual Check: Use browser_navigate to the post URL and check for an alert dialog or the injected <img> tag in the sidebar widget title.
  2. Database Check (WP-CLI):
    # Confirm the category name is stored
    wp term list category --fields=name,slug | grep "xss-cat"
    
  3. Response Analysis:
    Search the HTTP response body for the string onerror=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.
Research Findings
Static analysis — not yet PoC-verified

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

--- same-category-posts.php
+++ same-category-posts.php
@@ -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 `&lt;img...&gt;` 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.