Plugin Name | Plugin README Parser |
---|---|
Type of Vulnerability | Authenticated Stored XSS |
CVE Number | CVE-2025-8720 |
Urgency | Low |
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
Summary: A stored Cross-Site Scripting (XSS) vulnerability (CVE-2025-8720) affects the WordPress README Parser plugin versions up to and including 1.3.15. An authenticated user with Contributor (or higher) privileges can inject HTML/JavaScript which will be stored and later rendered, leading to script execution in the context of viewers (including administrators). This advisory explains the risk, realistic attack scenarios, detection techniques, and concrete mitigations and hardening steps you can apply immediately.
Prepared by a Hong Kong-based security researcher with incident-response and hardening experience. The guidance below is practical and prioritised for site owners, developers and operators.
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 and later rendered (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 administrators.
This is a stored (persistent) XSS vulnerability. Persistent XSS is more dangerous than reflected XSS because the injected script persists in storage and can affect multiple users repeatedly.
Why this matters to your WordPress site
- Contributor accounts are commonly available on community or multi-author sites. Contributors can often create and edit posts or provide metadata that plugins 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 forged admin requests).
- Install backdoors or webshells if combined with other vulnerabilities or social engineering.
- Display misleading content or redirect visitors.
- A successful stored XSS that runs in an admin’s browser can lead to full site takeover.
Who should read this
- Site owners running the README Parser plugin (<= 1.3.15).
- Administrators of multi-author blogs or membership sites where users have Contributor privileges.
- Developers and plugin authors seeking secure patterns to prevent similar issues.
- Web hosts and managed WordPress providers implementing host-level virtual patching.
Attack scenarios (realistic)
-
Community blog with open contributor sign-ups:
An attacker registers or obtains a contributor account and submits content or metadata with a crafted
target
payload containing scriptable HTML. When an administrator later visits the plugin admin page or a front-end page that renders the parsed README, the malicious script executes and can act in the admin’s context. -
Social-engineering an editor/author:
An attacker injects a payload that runs automatically when an editor previews or edits content; the script can perform privileged actions via XHR POSTs if CSRF protections are bypassed.
-
Mass distribution:
Because the injection is stored, every future viewer of the affected content (subscribers, editors, admins) may be impacted, increasing the blast radius.
What you should do now — step-by-step
If you run WordPress and have the README Parser plugin (<= 1.3.15) installed, follow these steps in order:
-
Immediate containment
- Restrict access to roles that can create or edit the plugin-affected fields. Temporarily disable public contributor registration if possible.
- If you have access controls, temporarily disallow untrusted accounts from reaching the admin pages used by the plugin.
-
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 removal is not possible, apply virtual patches or harden per the instructions below.
-
Apply virtual patch (WAF / firewall)
- Deploy rules to block malicious payloads in the
target
parameter or other inputs handled by the plugin. Example rules are provided later in this post.
- Deploy rules to block malicious payloads in the
-
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 DB queries to find entries with suspicious HTML (examples below).
- Check admin activity logs for unexpected account changes, new admin users, or plugin modifications.
- Search for recent changes to readme-like content or any fields processed by the plugin containing
-
Reset credentials
- Force password resets for administrators and consider invalidating active sessions. Rotate API keys for third-party integrations if applicable.
-
Post-incident: update plugin
- When an official fixed release is available, update immediately. If you removed the plugin, only reinstall after confirming the fix.
-
Review privileges and workflows
- Limit who can obtain Contributor or Editor roles and enforce review workflows that sanitize untrusted inputs before rendering.
Detection — what to look for
Search the database and logs for signs of stored XSS and related activity. Run queries from a trusted DBA context and ensure you have a backup.
Example SQL to find likely injected content:
-- 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:
- Requests with
target=
parameters containing encodedscript
strings:%3Cscript
,%3Cimg
,%3Con
,%3Ciframe
- 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 such as suspicious admin accounts created after suspected injection, unexpected plugin files, modified themes, or rogue cron jobs.
Practical hardening and developer fixes
If you maintain the README Parser plugin (or any plugin that accepts and renders user-supplied HTML), apply these secure coding practices:
-
Sanitize input on entry, escape on output
Sanitize user-supplied input when saving and escape at output. Use WordPress APIs:
sanitize_text_field()
,esc_html()
,esc_attr()
,esc_url()
, andwp_kses()
with an explicit whitelist. -
Use wp_kses for controlled HTML
If a limited subset of HTML is required, whitelist tags and attributes. Avoid allowing
on*
attributes orjavascript:
/data:
protocols.$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 );
-
Enforce capability checks and nonces
if ( ! current_user_can( 'edit_posts' ) ) { return; } if ( ! isset( $_POST['my_plugin_nonce'] ) || ! wp_verify_nonce( $_POST['my_plugin_nonce'], 'my_plugin_save' ) ) { return; }
-
Escape output in all contexts
Use
esc_attr()
for attributes,esc_html()
for text nodes, and only printwp_kses()
-sanitised HTML. -
Restrict fields that accept raw HTML
If
target
was intended as a slug or ID, treat it as such and do not accept HTML. -
Use Content Security Policy (CSP) as defence-in-depth
Apply a CSP header that disallows inline scripts and external untrusted sources. Test before roll-out to avoid breaking functionality.
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self';
-
Log and monitor content changes
Maintain an audit trail of posts and meta changes (user ID, timestamp) to speed 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) or host-level filtering is the fastest way to protect sites at scale. The rules below target common stored XSS payloads. Tune them to reduce 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 <script)
SecRule ARGS:target "(?i)(%253C|%26lt;).*script" "id:100004,phase:2,deny,status:403,msg:'Blocked double encoded XSS attempt',log"
NGINX (lua / 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 signatures: detect <script
, <img.*onerror
, on\w+\s*=
, javascript:
, encoded forms like %3Cscript
, and double-encoded sequences like %253C
or %25
patterns. Limit rules to the specific parameter(s) the plugin uses (e.g., target
) to reduce false positives.
If you operate an application-level filter, create a rule to forbid HTML tags or on*
attributes inside the target
parameter and either reject or sanitise them before saving.
Safe remediation code snippets (plugin-level fixes)
If you maintain the affected plugin and want a quick remediation before an upstream patch, sanitise the target
parameter on save and escape on output.
Sanitise before saving:
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:
$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 target
is used to build a URL, validate with esc_url_raw()
on save and esc_url()
on render.
Incident response — when you suspect compromise
If you find evidence of exploitation:
- Isolate the site: Put the site into maintenance mode and block public access if feasible.
- Snapshot and backup: Create a full backup (files and DB) before making changes.
- Clean injected content: Remove malicious HTML from posts, postmeta and options. Use SQL updates carefully and only after backing up.
- Audit users and reset credentials: Reset admin passwords, force password resets for privileged accounts, and revoke suspicious admin users.
- Scan for persistence: Check theme and plugin files for new or modified files, scheduled tasks (wp_cron), and wp-config.php for added code.
- Reinstall plugins/themes from trusted sources: Replace plugin files with fresh copies from the official WordPress repository after confirming the plugin version is untampered.
- Restore if necessary: If you cannot clean safely, restore from a known-good backup and apply WAF rules until a patch is available.
- Consider professional response: For large or sensitive sites, engage incident-response specialists.
Recommendations for site owners and hosts
- Limit Contributor capability where feasible. Require moderator review of submitted content on community sites.
- Enable multi-factor authentication for all administrators.
- Use host-level or application-level filtering that supports virtual patching while awaiting official fixes.
- Keep audit logs and activity monitoring active. Detecting suspicious admin page views after contributor updates is a key indicator.
- 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 stored data may be rendered in contexts that allow script execution (admin pages, front-end, REST responses).
- Provide escaping and sanitising options in code; do not rely solely on output context.
- Document expected input for each field and enforce validation on save.
- Consider storing both raw data and a sanitized/rendered variant — ensure the sanitized variant is used for presentation.
- Conduct threat modelling: 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 additional defence that reduces the impact of XSS by disallowing inline scripts and restricting script origins. Implementing a strict CSP may require refactoring; however, a moderate CSP (for example, script-src 'self'
and forbidding unsafe-inline
) raises the bar for exploitation.
Note: CSP complements but does not replace proper input sanitisation and escaping.
Recovery checklist (quick)
- Deactivate/remove README Parser plugin (if possible) or restrict access
- Apply WAF signatures blocking
target
payloads (see examples) - Search DB for suspicious HTML and clean
- Rotate admin passwords and revoke sessions
- Audit users and recent admin actions
- Reinstall plugin from the official repository after an official fix
- Apply developer hardening measures to plugin code
- Add a CSP header as defence-in-depth
- Enable monitoring to detect future attempts
Example: Minimal aggressive ModSecurity rule to block target
parameter
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
- If you can remove the plugin without impacting functionality: do so immediately.
- If removal is not possible: deploy targeted WAF rules to block the
target
parameter from carrying script-like content and monitor logs carefully. - Audit and clean the database for injected content.
- Short-term: restrict contributor signups and limit privileges.
- Mid-term: patch plugin code using
wp_kses()
and strict capability/nonces; long-term: adopt CSP and continuous monitoring.
Stored XSS remains a frequent and serious issue because it combines persistent data with contexts that can be powerful (administrator browsers). Defend in layers: remove or update vulnerable software, sanitise input and escape output rigorously, enforce least privilege for users, and apply targeted virtual patching while waiting for upstream fixes.
— Hong Kong Security Researcher