| Plugin Name | WPBakery Page Builder |
|---|---|
| Type of Vulnerability | Stored XSS |
| CVE Number | CVE-2025-11161 |
| Urgency | Low |
| CVE Publish Date | 2025-10-15 |
| Source URL | CVE-2025-11161 |
WPBakery Page Builder <= 8.6.1 — Stored XSS via vc_custom_heading shortcode (CVE-2025-11161): What WordPress site owners must do now
Published: 15 October 2025 | Severity: CVSS 6.5 (Medium / Low patch priority)
Affected: WPBakery Page Builder plugin versions ≤ 8.6.1 | Fixed in: 8.7 | CVE: CVE-2025-11161 | Reported by: independent researcher
As a Hong Kong-based security expert who regularly advises site owners and operators across APAC, I’ll give a clear, practical guide to this vulnerability: the real-world risks, detection techniques, and immediate mitigations you must consider. This is a pragmatic defender-focused write-up you can act on whether you run a single blog or manage dozens of client sites.
Scope of this post:
- What exactly is wrong and why it matters
- Who is at risk and realistic exploitation scenarios
- How to find whether your site is vulnerable or already injected
- Immediate and layered mitigations: update, virtual patching/WAF rules, content sanitization and hardening
- Incident response if you discover an infection
Executive summary
- This is a stored cross-site scripting (XSS) vulnerability in the vc_custom_heading shortcode of WPBakery Page Builder versions ≤ 8.6.1. The plugin may render user-supplied heading content without adequate sanitization or escaping.
- Fixed in WPBakery Page Builder 8.7. Upgrading to 8.7+ is the primary long-term fix.
- Immediate mitigations: apply virtual patches or WAF rules, remove or sanitize dangerous shortcode content, audit contributor-created content, and harden user privileges.
- If you suspect compromise: isolate the site, preserve evidence, scan and clean the site, and rotate credentials.
Technical background — root cause explained
Shortcodes allow plugins to expand tokens like [vc_custom_heading] into HTML during content rendering. WPBakery Page Builder exposes many such shortcodes. The root cause here is a stored XSS pattern:
- A user with permission to create or edit content (disclosure indicates Contributor or higher) inserts a crafted payload into a shortcode attribute or content field managed by
vc_custom_heading. - The plugin stores that content in the database (post content or post meta).
- On render, the plugin outputs the stored value into HTML without proper escaping or with a permissive filter that allows script-capable attributes (inline handlers, javascript: URIs, etc.).
- When a visitor or admin views the page, the malicious script executes in their browser context.
Stored XSS is persistent: injected payloads remain until removed. Required privilege (Contributor) is notable — low-privilege accounts or site registrations are often the path of exploitation.
Realistic exploitation scenarios
- A malicious registered user creates a post using WPBakery elements and places a payload in the heading field. The published page executes JavaScript in visitors’ browsers, including admins who view it.
- A compromised contributor account injects payloads into high-traffic pages to maximise reach and persistence.
- An attacker crafts payloads that make background requests to admin endpoints (admin-ajax.php or REST API) using the victim’s authenticated cookies — potentially creating admin users, changing settings, or uploading a backdoor if endpoints allow it.
- Payloads for SEO poisoning, redirects, credential phishing, cryptomining or drive-by malware delivery.
Stored XSS can lead to full site takeover when admins view a poisoned page. It’s a privacy, trust and operational risk.
Who is at risk?
- Sites running WPBakery Page Builder ≤ 8.6.1.
- Sites that allow users with Contributor or higher roles to publish or save content (membership sites, multi-author blogs, vendor platforms).
- Sites that cannot or have not yet patched to 8.7+ and that lack virtual patching or effective content sanitization.
How to check your site — discovery & detection
Confirm presence and version of WPBakery Page Builder first.
- Check plugin version
- WordPress admin: Plugins → Installed Plugins → find WPBakery Page Builder.
- If admin access is unavailable, inspect files on the server or readme files. Prefer server-side inspection to avoid remote fingerprinting errors.
- Identify posts using the vulnerable shortcode
Search for posts containing
vc_custom_headingor suspicious attributes.SQL (run carefully on a staging copy):
SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%vc_custom_heading%';To find script-like content:
SELECT ID, post_title FROM wp_posts WHERE post_content REGEXP '<(script|img|iframe|svg|object|embed)[[:space:]]|onerror=|onload=|javascript:';WP-CLI options for bulk environments:
wp db export - && grep -R "vc_custom_heading" -n - Search post meta
Page builders often store config in
wp_postmeta. Example:SELECT post_id, meta_key FROM wp_postmeta WHERE meta_value LIKE '%<script%' OR meta_value LIKE '%onerror=%' OR meta_value LIKE '%vc_custom_heading%'; - Log and traffic indicators
- Analytics: abnormal outbound requests or unusual referrer patterns.
- Unexpected admin users, suspicious scheduled tasks, or new uploads.
- Scanning
Run content and file scanners that detect inline JavaScript in posts and post meta. If you already operate a WAF with virtual patching, check its logs for blocked attempts.
Always test queries and remediation on a backup or staging copy before changing production data.
Immediate steps you must take (triage)
Prioritise these actions:
- Update WPBakery Page Builder to 8.7 or later immediately where possible. This is the definitive fix.
- If you cannot update immediately (compatibility testing required), apply layered mitigations while you prepare the plugin update:
- Deploy virtual patching using WAF rules to block exploit attempts targeting
vc_custom_headingand suspicious attributes. - Temporarily restrict contributors’ ability to publish or use page-builder tools until you confirm site cleanliness.
- Audit and sanitize content created by Contributor/Author accounts; unpublish affected pages if necessary.
- Deploy virtual patching using WAF rules to block exploit attempts targeting
- Audit content — search posts/pages and post meta for
vc_custom_headingand event-handler attributes. Remove or sanitize detected payloads. - Harden privileges — require review by an editor/admin for content from non-trusted users; reduce publish rights.
- Rotate secrets and sessions — reset passwords for admin users if any suspicion exists and invalidate active sessions where possible.
- Backup and scan — take a full backup (files + DB) and then run content/file scans and manual inspections.
Example WAF rules and virtual patching guidance
If you operate a WAF, ModSecurity or NGINX with request inspection, you can deploy rules to block exploit attempts. Test rules in detection mode on staging first to avoid false positives.
ModSecurity example (conceptual):
# Block attempts to submit vc_custom_heading with inline script or event attributes
SecRule REQUEST_BODY|ARGS|ARGS_NAMES "vc_custom_heading" "phase:2,deny,log,status:403,id:100001,msg:'Block attempt to exploit vc_custom_heading stored XSS',chain"
SecRule REQUEST_BODY|ARGS "(<script\b|onerror=|onload=|javascript:)" "t:none,chain"
SecRule REQUEST_METHOD \"POST\" \"t:none\"
NGINX (simplified logic):
if ($request_method = POST) {
set $block 0;
if ($request_body ~* "vc_custom_heading") {
if ($request_body ~* "(<script\b|onerror=|onload=|javascript:)") {
set $block 1;
}
}
if ($block = 1) {
return 403;
}
}
WordPress-level temporary fix: an mu-plugin that sanitizes post content on save by stripping script tags and event handlers. Conceptual example (test carefully):
<?php
/*
Plugin Name: Temporary vc_custom_heading sanitizer (mu)
Description: Virtual patch - remove potentially dangerous attributes from vc_custom_heading
*/
add_filter('content_save_pre', 'vc_heading_virtual_patch', 10, 1);
function vc_heading_virtual_patch($content) {
if (stripos($content,'vc_custom_heading') === false) {
return $content;
}
// Remove script tags and event handlers
$content = preg_replace('#<script(.*?)&(amp;)?gt;(.*?)</script>#is', '', $content);
$content = preg_replace('/\s(on\w+)\s*=\s*"[^"]*"/i', '', $content); // strips onerror="..." inline handlers
$content = preg_replace("/javascript:/i", "", $content);
return $content;
}
Note: the mu-plugin above is a stop-gap. It aims to neutralise known dangerous patterns, but it does not replace a proper plugin update and secure output escaping. Test before deploying to production.
Sanitization and developer guidance (how the plugin should change)
Developer-level fixes should apply defence-in-depth:
- Escape all user-controlled values at output using the correct escaping function (esc_html(), esc_attr(), esc_url()).
- Whitelist allowed HTML use wp_kses() with a strict allowed elements and attributes list for any HTML allowed inside a shortcode.
- Do not echo raw user input inside attributes that permit event handlers (on*) or javascript: URIs.
- Sanitize data on save as an additional safeguard, but always escape on output.
Example safe rendering strategy for a heading shortcode:
$allowed_tags = array(
'strong' => array(),
'em' => array(),
'br' => array(),
'span' => array('class' => true),
'a' => array('href' => true, 'rel' => true, 'target' => true)
);
$safe_text = wp_kses( $raw_text, $allowed_tags );
echo '<h2 class="'.esc_attr($class).'">'.wp_kses_post($safe_text).'</h2>';
Hunting for injected content (practical queries & regex)
- Find script tags inside posts:
SELECT ID, post_title FROM wp_posts WHERE post_content REGEXP '<script[[:space:]]'; - Locate event-handler attributes:
SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%onerror=%' OR post_content LIKE '%onload=%' OR post_content LIKE '%onclick=%'; - Search post meta:
SELECT post_id, meta_key FROM wp_postmeta WHERE meta_value REGEXP '<script|onerror=|onload='; - Grep exported content:
grep -R --line-number -E "(vc_custom_heading|onerror=|<script|javascript:)" wp-content
When you find suspicious content, export that post to a safe environment and inspect carefully. If unsure, restore from a verified pre-infection backup.
If you find a compromise — incident response checklist
- Isolate and preserve
- Put the site into maintenance mode or block inbound traffic to limit damage.
- Make a full forensic backup: files + database; preserve timestamps and logs.
- Take screenshots and save logs for later analysis.
- Identify scope
- Which pages, users and uploads were modified?
- Check for new admin users and unexpected cron entries.
- Inspect uploads and code for webshells or modified PHP files.
- Clean & restore
- Remove injected content or restore clean versions from verified backups.
- Replace core, plugin and theme files with fresh copies from trusted sources.
- Remove unknown users and rotate passwords (admin accounts, FTP, database, hosting panel).
- Strengthen
- Update all software components (plugins, themes, core).
- Harden admin access: 2FA for admins, limit login attempts, IP restrictions for wp-admin where feasible.
- Apply virtual patching and confirm attacks are blocked.
- Monitor and verify
- Maintain enhanced logging for 30 days and monitor for re-infection.
- Scan files and database weekly for anomalies for a monitoring period.
- Engage professional incident responders for extensive compromises.
- Post-incident review
- Conduct root cause analysis: how was the contributor account created or hijacked?
- Update policies and workflows to reduce future risk.
Long-term hardening and best practices
- Keep WPBakery and all plugins/themes up to date.
- Principle of least privilege — only grant Contributor or higher when necessary.
- Use an editorial workflow plugin or review process for untrusted contributors.
- Limit or sanitize page builder usage by untrusted roles; strip shortcodes on save when appropriate.
- Use wp_kses() and strict sanitizers where user content is allowed.
- Maintain automated daily backups and regularly test restores.
- Deploy WAF/virtual patching and continuous malware scanning as part of a layered defence.
- Implement file integrity monitoring to detect unexpected changes early.
Practical remediation playbook (step-by-step)
- Backup now: full backup of files and DB; store offsite.
- Update WPBakery Page Builder to 8.7+ on a staging copy and verify functionality.
- Test plugin updates in staging; deploy to production when verified.
- If immediate update is not possible:
- Deploy WAF rules or virtual patches to block exploit traffic.
- Add a mu-plugin that strips event handlers and script tags on save (temporary).
- Restrict contributor publishing or disable page-builder access for untrusted roles.
- Search & clean using the SQL/grep queries above; restore clean backups for affected posts where feasible.
- Rotate credentials and terminate admin sessions.
- Monitor closely for at least 30 days post-remediation.
Sample detection regexes and admin workflows
Regex to find common inline event handlers and javascript: URIs:
/(on\w+\s*=|<script\b|javascript:)/i
Recommended admin workflow:
- Create a “content review” role and require two-person review for pages containing shortcodes.
- Flag content with
vc_custom_headingfor manual review and provide a quick quarantine option.
Closing notes — practical takeaways
- Upgrade WPBakery Page Builder to 8.7+ as soon as possible — this is the definitive fix for CVE-2025-11161.
- In parallel, deploy WAF rules or server-side filters to block exploit payloads and sanitize content created by untrusted users.
- Hunt for injected content using the SQL, WP-CLI and grep patterns above. Clean or restore affected content and rotate credentials if you find malicious content.
- Reconsider contributor workflows and reduce the blast radius of non-admin roles. Enforce content review and sanitize content at both save and output time.
- If the site is business-critical or you are unsure about cleanup, engage a professional incident response team experienced with WordPress compromises.