WordPress Authenticated Stored XSS in Readme Parser//Published on 2025-08-15//CVE-2025-8720

ĐỘI NGŨ BẢO MẬT WP-FIREWALL

README Parser Plugin Vulnerability

Tên plugin Plugin README Parser
Type of Vulnerability Authenticated Stored XSS
CVE Number CVE-2025-8720
Tính cấp bách Thấp
CVE Publish Date 2025-08-15
Source URL CVE-2025-8720

Authenticated Contributor Stored XSS in README Parser (≤ 1.3.15) — What Site Owners and Developers Must Do Now

Bản tóm tắt: A stored Cross-Site Scripting (XSS) vulnerability identified as CVE-2025-8720 affects the WordPress plugin “README Parser” versions up to and including 1.3.15. The flaw allows an authenticated user with Contributor (or higher) privileges to inject HTML/JavaScript into content that will later be rendered and executed. This post explains the risk, likely attack scenarios, how to detect if your site is impacted, and concrete mitigation and hardening steps — including sample WAF/virtual-patch rules you can apply right away.

We are the WP-Firewall security team. We protect thousands of WordPress sites and speak from both incident-response and long-term-hardening perspectives. Below you’ll find pragmatic advice for site owners, developers and plugin maintainers.


Quick facts

  • Vulnerability: Stored Cross-Site Scripting (XSS)
  • Affected software: README Parser plugin for WordPress
  • Vulnerable versions: ≤ 1.3.15
  • CVE: CVE-2025-8720
  • Required privileges to exploit: Contributor or higher
  • Severity / CVSS: Medium (reported CVSS 6.5)
  • Official fix: Not available at publication time (apply mitigation)
  • Date published: 15 August 2025
  • Reporter credit: Researcher(s) who disclosed responsibly

What happened — plain language

The README Parser plugin accepts a parameter named target that can carry HTML content or data used to build a README-like output. In versions up to 1.3.15, the plugin does not properly sanitize or encode untrusted input from authenticated users with Contributor privileges. Because that content is stored (persisted) and then rendered later (server-side or client-side), a malicious contributor can insert HTML or JavaScript which will execute in the context of anyone who views the rendered output — including site administrators.

This is a stored XSS (persistent) vulnerability and is more dangerous than reflected XSS because the injected script lives in the database and can affect multiple users repeatedly.


Why this matters to your WordPress site

  • Contributor accounts are commonly available on community or multi-author sites. While contributors cannot normally publish content, they can create and edit posts and, in some workflows, input metadata or other fields that a plugin may parse.
  • Stored XSS can be used to:
    • Steal administrator session cookies or authentication tokens (if protections are weak).
    • Perform actions on behalf of an authenticated victim (via CSRF or forged admin requests).
    • Install backdoors or webshells if combined with other vulnerabilities or social engineering.
    • Display misleading content or redirects on pages where visitors land.
  • A successful stored XSS against an admin viewing the injected data can lead to full site takeover.

Who should read this

  • Site owners running the README Parser plugin (≤1.3.15).
  • Administrators responsible for multi-author blogs or membership sites where users have Contributor privileges.
  • Developers and plugin authors looking for secure coding patterns to prevent similar issues.
  • Web hosts and managed WordPress providers implementing host-level virtual patching.

Attack scenarios (realistic)

  1. Community blog with open contributor sign-ups:
    • Attacker registers or obtains a contributor account.
    • Submits content or metadata with a crafted target parameter payload that contains scriptable HTML.
    • Later, an administrator visits the plugin’s admin page or the front-end page that renders the parsed README — the malicious script executes in the admin’s browser, performing privileged actions.
  2. Social-engineering to get an editor/author to view a specific page:
    • Attacker injects payload that runs automatically when an editor previews or edits content. The script can quietly perform actions via the admin interface (create admin user, change settings) by issuing XHR POSTs if CSRF protections can be bypassed.
  3. Mass distribution:
    • Since the injection is stored, the malicious content affects every future viewer of that content (subscribers, editors, admins), increasing the potential blast radius.

What you should do now — step-by-step

If you run WordPress and have the README Parser plugin (≤ 1.3.15) installed, take these actions in order:

  1. Immediate containment
    • Restrict access to user roles that can create or edit the plugin-affected fields. Temporarily disable public contributor registration if possible.
    • If you have monitoring or an access-control list, temporarily disallow non-trusted accounts from reaching the admin pages used by the plugin.
  2. Remove or deactivate the plugin (if you do not need it)
    • If the plugin is not critical, deactivate and remove it until an official patch is released.
    • If removing is not possible, continue to virtual-patch / harden per the instructions below.
  3. Apply virtual patch (WAF / firewall)
    • Deploy WAF rules to block malicious payloads in the target parameter or other inputs handled by the plugin. Examples of effective rules are included later in this post.
  4. Audit the database and admin users
    • Search for recent changes to readme-like content or any fields processed by the plugin containing <script, onerror=, javascript:, or other suspicious tokens.
    • Run a DB query to find entries with <script or suspicious HTML (example SQL provided below).
    • Check admin activity logs and look for unexpected account changes, new admin users, or plugin modifications.
  5. Reset credentials
    • Force password reset for administrators and consider invalidating all active sessions. Rotate API keys if any third-party integrations exist.
  6. Post-incident: update plugin
    • When an official fixed release is available, update immediately. If you removed the plugin, only re-install after confirming the fix.
  7. Review privileges and workflows
    • Limit who can obtain Contributor or Editor roles.
    • Move to review workflows that ensure all untrusted uploads and meta fields are sanitized before being rendered.

Detection — what to look for

Search the database and logs for signs of stored XSS and related activity.

Example SQL to find likely injected content (run from a trusted DBA context; backup DB first):

-- Search post content and postmeta for script tags or on* attributes
SELECT ID, post_title, post_date
FROM wp_posts
WHERE post_content LIKE '%<script%';
 
SELECT post_id, meta_key, meta_value
FROM wp_postmeta
WHERE meta_value LIKE '%<script%' OR meta_value LIKE '%onerror=%' OR meta_value LIKE '%javascript:%';

Search access logs for suspicious query strings:

  • Look for requests with target= parameters containing encoded script strings: %3Cscript, %3Cimg, %3Con, %3Ciframe
  • Look for POSTs creating or editing content from low-privilege accounts.

Log indicators:

  • Admin pages returning success on actions shortly after a contributor edit.
  • Multiple previews or admin views for a particular post by administrators after a contributor update.

Look for indicators of compromise (IoCs) such as suspicious admin accounts created after the suspected injection, unexpected plugin files, modified themes, or cron jobs.


Practical hardening and developer fixes

If you maintain the README Parser plugin (or any plugin that accepts and renders user-supplied HTML), here are safe coding practices:

  1. Sanitize input on entry, escape on output
    • Never assume content is safe. Sanitize user-supplied input immediately when saving, and escape at output.
    • Use WordPress APIs: vệ sinh trường văn bản(), esc_html(), esc_attr(), esc_url(), Và wp_kses() with allowed HTML tags.
  2. Use wp_kses for controlled HTML
    <?php
    $allowed = array(
      'a' => array(
        'href' => true,
        'title' => true,
        'rel'   => true,
      ),
      'br' => array(),
      'em' => array(),
      'strong' => array(),
      'p' => array(),
      'ul' => array(),
      'li' => array(),
    );
    $clean_html = wp_kses( $input, $allowed );
    ?>
    

    Avoid allowing on* attributes (e.g., nhấp chuột, onerror) or javascript:/data: protocols.

  3. Enforce capability checks and nonces
    <?php
    if ( ! current_user_can( 'edit_posts' ) ) {
      return;
    }
    if ( ! isset( $_POST['my_plugin_nonce'] ) || ! wp_verify_nonce( $_POST['my_plugin_nonce'], 'my_plugin_save' ) ) {
      return;
    }
    ?>
    
  4. Escape output in all contexts
    • When you output values into HTML attributes: use esc_attr().
    • When you output URLs: use esc_url_raw() when saving, esc_url() when printing.
    • When you output HTML: only print wp_kses()-sanitized HTML.
  5. Restrict fields that accept raw HTML

    If the target parameter was intended only for internal routing or plain text, convert it to a slug or ID. Never treat it as HTML.

  6. Use Content Security Policy (CSP) as defense-in-depth

    Apply a CSP header that disallows inline scripts and external untrusted sources. Note: CSP may break some legitimate functionality if configured too strictly, so test before roll-out.

    Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self';
  7. Log and monitor content changes

    Keep an audit trail of posts and meta changes, including the user ID and timestamp. That speeds investigation if something is injected.


Virtual patching / WAF rules you can deploy now

If an official plugin update is not yet available, virtual patching via a Web Application Firewall (WAF) is the fastest way to protect sites at scale. Below are example rules for ModSecurity (compatible with many WAFs) and NGINX (Lua or ngx_http_rewrite_module style) that target malicious payloads commonly used in stored XSS.

Important: These are defensive filters — tune them to avoid false positives on sites that legitimately allow HTML.

Example ModSecurity rule set (conceptual):

# Block suspicious script tags in 'target' parameter (URL or POST data)
SecRule ARGS:target "(?i)(%3C|<)\s*script" "id:100001,phase:2,deny,status:403,msg:'Blocked XSS attempt - script tag in target',log"

# Block javascript: and data: in URL-like inputs
SecRule ARGS:target "(?i)javascript:|data:text/html" "id:100002,phase:2,deny,status:403,msg:'Blocked XSS attempt - protocol in target',log"

# Block common on* event attributes inside parameters (encoded or plain)
SecRule ARGS:target "(?i)on\w+\s*=" "id:100003,phase:2,deny,status:403,msg:'Blocked XSS attempt - event handler attribute in target',log"

# Block suspicious encoded payloads (double-encoded 

NGINX (using ngx_lua or a similar approach) — pseudocode:

# if ngx_lua available, inspect request args
access_by_lua_block {
  local args = ngx.req.get_uri_args()
  local target = args["target"]
  if target then
    local lower = string.lower(target)
    if string.find(lower, "<script") or string.find(lower, "javascript:") or string.find(lower, "onerror=") then
      ngx.log(ngx.ERR, "Blocked XSS attempt in target: " .. target)
      ngx.exit(ngx.HTTP_FORBIDDEN)
    end
  end
}

Regex tips for signature creation:

  • Detect <script, <img.*onerror, on\w+\s*=, javascript:, and encoded forms %3Cscript, %3Cimg, %26lt;script, %253C.
  • Include checks for commonly double-encoded payloads (%25 sequences).
  • Limit the rules to the specific parameter(s) the plugin uses (e.g., target) to reduce false positives.

If you run a WordPress-level firewall plugin (server or plugin-based), create a rule to forbid HTML tags or on* attributes inside the target parameter and to reject or sanitize them before saving.


Safe remediation code snippets (plugin-level fixes)

If you maintain the affected plugin yourself and want a quick remediation before an upstream patch, sanitize the target parameter on save and escape on output.

Example PHP: sanitize before saving

<?php
if ( isset( $_POST['target'] ) ) {
    // Remove HTML tags entirely if this parameter is meant to be plain text
    $target_clean = sanitize_text_field( wp_unslash( $_POST['target'] ) );

    // OR: allow only safe HTML using wp_kses
    $allowed = array( 'a' => array( 'href' => true, 'title' => true ) );
    $target_clean = wp_kses( wp_unslash( $_POST['target'] ), $allowed );

    update_post_meta( $post_id, 'plugin_readme_target', $target_clean );
}
?>

Output with safety:

<?php
$stored = get_post_meta( $post_id, 'plugin_readme_target', true );
// Use esc_attr if printing into an attribute, or esc_html if in text node
echo esc_html( $stored );
?>

If the target value is used to build an URL, explicitly validate with esc_url_raw() on save and esc_url() on render.


Incident response — when you suspect compromise

If you find evidence of exploitation:

  1. Isolate the site
    • Take the site into maintenance mode, block public access if feasible.
  2. Snapshot and backup
    • Create a full backup (files and DB) before you change anything.
  3. Clean the injected content
    • Remove any discovered malicious HTML from posts, postmeta and options.
    • Use SQL updates carefully to remove script tags (backup DB first).
  4. Audit users and reset credentials
    • Reset admin passwords and force password reset for privileged accounts.
    • Revoke any suspicious admin users.
  5. Scan for persistence
    • Check theme and plugin files for new or modified files.
    • Check scheduled tasks (wp_cron) and wp-config.php for added code.
  6. Reinstall plugins/themes from trusted sources
    • Replace plugin files with fresh copies from the WordPress repo after you confirm the plugin version is free of tampering.
  7. If you cannot clean safely, restore from a known-good backup and apply the WAF rule until a patch is available.
  8. Consider professional incident response for large or sensitive sites.

Recommendations for site owners and hosts

  • Limit Contributor capability where feasible. If you operate a community site, require moderator review of submitted content.
  • Enable multi-factor authentication for all administrators.
  • Use a security plugin or host-level firewall that supports virtual patching. Virtual patching blocks exploitation patterns even when no official patch exists.
  • Keep audit logs and activity monitoring active. Detecting suspicious admin page views after contributor updates is a key detection strategy.
  • Educate editors and admins to avoid previewing untrusted content in admin consoles until content has been sanitized or reviewed.

For plugin authors — guidelines to prevent similar issues

  • Treat all user input as hostile, even from authenticated users.
  • Assume that any stored data can be rendered in contexts that allow script execution (admin pages, front-end, REST responses).
  • Provide escaping and sanitizing options in the code; never rely solely on output context.
  • Document expected input for each field and enforce validation on save.
  • Adopt a model that stores raw data and a sanitized/rendered variant — but ensure the sanitized variant is what's used for presentation.
  • Conduct threat modeling: consider where saved plugin metadata might later be rendered in admin screens accessed by privileged users.

Example detection regexes and DB-SQL queries

Quick regex examples (for log scanning or SIEM):

  • Detect script tag: (?i)(<|%3[cC])\s*script
  • Detect event handlers: (?i)on[a-z]+\s*=
  • Detect javascript: protocol: (?i)javascript\s*:
  • Detect double-encoding: (?i)%25[0-9a-f]{2}

SQL search examples:

-- Find meta values with html/script content
SELECT meta_id, post_id, meta_key, meta_value
FROM wp_postmeta
WHERE meta_value REGEXP '(?i)<script|on[a-z]+=|javascript:';

-- Find posts with script tags
SELECT ID, post_title, post_date
FROM wp_posts
WHERE post_content REGEXP '(?i)<script|on[a-z]+=|javascript:';

What about Content Security Policy (CSP) and browser defenses?

CSP is a powerful last-line defense that reduces the impact of XSS. An ideal CSP disallows inline-scripts and only allows scripts from trusted origins. For many WordPress sites, implementing a strict CSP may require refactoring. However, even a moderate CSP that sets script-src 'self' and forbids unsafe-inline significantly raises the bar for exploitability.

Note: CSP is not a substitute for proper input sanitization and escaping. It complements server-side defenses and WAF virtual patching.


Recovery checklist (quick)


Example: Minimal aggressive ModSecurity rule to block target tham số

Use with caution — test for false positives.

SecRule REQUEST_METHOD "@streq POST" "id:100200,phase:2,pass,nolog,chain"
  SecRule ARGS:target "(?i)(%3C|<)\s*(script|img|iframe|svg|object)|javascript:|on[a-z]{1,20}\s*=" "id:100201,phase:2,deny,status:403,msg:'Aggressive protection - blocked possible stored XSS in target parameter'"

This drops POSTs that include script-like content in target. If your site legitimately posts HTML in target, use a less aggressive rule that logs and alerts first.


Timeline and disclosure notes

  • Vulnerability published: 15 August 2025
  • CVE assigned: CVE-2025-8720
  • Required privilege: Contributor (authenticated)
  • Official vendor patch: Not available at time of writing — follow the vendor’s update channels and apply this guidance until a patch is released.

Final recommendations — prioritized

  1. If you can remove the plugin without impacting functionality: do so immediately.
  2. If removal is not possible: deploy WAF rules to block the target parameter from carrying script-like content and monitor logs carefully.
  3. Audit and clean the database for injected content.
  4. Short-term: restrict contributor signups and limit privileges.
  5. Mid-term: patch plugin code using wp_kses() and strict capability/nonces; long-term: adopt CSP and monitoring.

Protect your WordPress site with our free protection — Start here

We know how disruptive vulnerabilities like CVE-2025-8720 can be. If you want essential, hands-off protection while you apply fixes, sign up for WP-Firewall’s Basic (Free) plan. It includes managed firewall rules, a Web Application Firewall (WAF), malware scanning, unlimited bandwidth, and coverage for OWASP Top 10 threats — so you can stop attacks at the edge while you investigate.

Explore the Basic plan and get protected today: https://my.wp-firewall.com/buy/wp-firewall-free-plan/

If you need automated malware removal, blacklist/whitelist controls or monthly security reporting, see our Standard and Pro plans for extended features.


Closing notes from the WP-Firewall security team

Stored XSS continues to be a common and dangerous class of vulnerability because it combines persistent data with user contexts that can be powerful (administrator browsers). The best protection is layered: remove or update vulnerable software, sanitize input and escape output in code, enforce least privilege for users, and add virtual patching via a WAF so you are protected while waiting on upstream fixes.

If you want assistance implementing WAF rules, scanning your site for signs of compromise, or applying virtual patches across multiple sites, our incident response team can help. We also offer automated virtual patching to protect applications in real-time without waiting for plugin updates.

Stay safe, audit often, and treat user-supplied content as hostile — especially on multi-author sites.

— WP-Firewall Security Team


wordpress security update banner

Nhận WP Security Weekly miễn phí 👋
Đăng ký ngay
!!

Đăng ký để nhận Bản cập nhật bảo mật WordPress trong hộp thư đến của bạn hàng tuần.

Chúng tôi không spam! Đọc của chúng tôi chính sách bảo mật để biết thêm thông tin.