How to Fix Recent WordPress Plugin Vulnerabilities Across JetFormBuilder, Better Messages, OpenStation, Customer Reviews, and Bookly
September 25, 2026· 8 min read
TL;DR: Update each affected plugin to the version that contains the official security fix. If you cannot update immediately, drop the short mu‑plugin patches shown in the sections below. Hard‑en your update pipeline – automate scans, pin versions, and keep a reliable staging copy so future bugs are caught early.
TL;DR Summary
✔️Update each affected plugin to the version that contains the official security fix.
✔️If you cannot update immediately, drop the short mu‑plugin patches shown in the sections below.
✔️Hard‑en your update pipeline – automate scans, pin versions, and keep a reliable staging copy so future bugs are caught early.
1. Quick Glossary
1. Quick Glossary
Term
What it means
------
---------------
XSS (Cross‑Site Scripting)
An attacker injects JavaScript that runs in another user’s browser, usually by tricking the server into echoing unsanitised data.
SQL Injection
Malicious data is inserted into a database query, allowing the attacker to read, modify, or delete data that should be protected.
CVE
A public identifier for a known security flaw (e.g., CVE‑2026‑92212).
mu‑plugin
A “must‑use” plugin that lives in wp‑content/mu‑plugins. It is loaded before normal plugins and cannot be deactivated from the admin UI.
Capability check
WordPress function (currentusercan(), user_can()) that verifies a user’s role/permission before allowing a state‑changing operation.
Nonce
A one‑time token (wpcreatenonce()) used to protect against CSRF attacks on admin‑side AJAX or form submissions.
2. Why These Five Plugins Matter
All five plugins are high‑traffic, front‑facing components that sit on top of core WordPress functionality:
Plugin
Typical Use‑Case
Approx. Install Base (2024)
--------
------------------
-----------------------------
JetFormBuilder
Drag‑and‑drop form builder for contact, lead‑gen, and surveys.
30 000+ active sites
Better Messages
Real‑time chat & messaging for BuddyPress / bbPress.
12 000+ active sites
OpenStation
AI‑assisted content generation and workflow automation.
5 000+ active sites
Customer Reviews for WooCommerce
Collects and displays post‑purchase reviews, incentivises rating.
20 000+ active sites
Bookly
Appointment booking & calendar integration for service‑based businesses.
40 000+ active sites
Because they touch user‑generated content, AJAX endpoints, and admin settings, a single flaw can give an attacker persistent access, data exfiltration, or financial loss (e.g., unwanted AI API calls).
3. Preparing Your Environment Before You Patch
3. Preparing Your Environment Before You Patch
Back up everything – database dump (wp db export) and file system (tar -czf wp-backup.tar.gz /var/www/html). Store the backup off‑site.
Spin up a staging copy – clone the live site to a sub‑domain (staging.example.com) or a local Docker container. Use the same PHP, MySQL/MariaDB, and web‑server configuration as production.
Enable WP_DEBUG in wp-config.php for the staging site:
Install an activity‑log plugin on both environments. It will record every updateoption(), wpinsertpost(), and wpajax_* call, giving you a forensic baseline.
4. JetFormBuilder – Stored XSS (CVE‑2026‑92212)
4.1 What Happened
JetFormBuilder stored form field labels ($fieldlabel) directly in the wpjfbformfields table. When the admin list view rendered each row, the code echoed the label without escaping:
php
echo '<td>' . $field_label . '</td>';
If a malicious user saved a label like , the script would be stored verbatim. Every admin who later opened the Forms → All Forms screen would trigger the alert, because the string was printed raw into the page’s HTML.
4.2 Why It Matters
✔️Persistence – The payload lives in the database, surviving plugin updates, theme changes, and even a full site migration.
✔️Privilege escalation – Only admins see the list, but many sites grant “editor” or “shop manager” roles the ability to edit forms. If those roles lack the unfiltered_html capability, the XSS still runs because the output occurs after WordPress’s core sanitisation.
✔️Change: All label output now passes through eschtml(). The plugin also adds a sanitizetext_field() call on save, preventing script tags from being persisted.
✔️Trade‑off – The official fix raises the minimum WordPress requirement to 5.9. If you run an older core version, you must either upgrade WordPress first or apply the mu‑plugin workaround.
4.4 Manual Fix (mu‑plugin)
Create a file wp-content/mu-plugins/jfb-xss-patch.php with the following content:
php
<?php
/**
* Plugin Name: JetFormBuilder XSS Patch (pre‑upgrade)
* Description: Escapes field labels in the admin list view to mitigate CVE‑2026‑92212.
* Author: Your Name / Security Team
* Version: 1.0
*/
add_action( 'admin_init', function () {
// Bail out if JetFormBuilder is not active.
if ( ! class_exists( 'Jet_Form_Builder' ) ) {
return;
}
// Hook into the output buffer used by the plugin.
// The original plugin uses a filter `jfb_form_fields_table_row`.
add_filter( 'jfb_form_fields_table_row', function ( $html, $field ) {
// $field['label'] is the raw DB value.
$escaped = esc_html( $field['label'] );
// Re‑build the <td> element.
return '<td>' . $escaped . '</td>';
}, 10, 2 );
} );
4.5 Cleaning Existing Entries
Run the following WP‑CLI command on the staging site (replace example.com with your domain):
✔️Dry‑run first – verify the number of replacements.
✔️Remove the --dry-run flag once you are satisfied.
If you have multiple variants (.
Navigate to Forms → All Forms.
Expected result – The label appears as literal text () and no alert pops up.
Check the DB – run SELECT label FROM wpjfbform_fields WHERE label LIKE '%script%'; – you should see the raw string, confirming the patch does not delete data, only neutralises it.
✔️Change: All public AJAX endpoints now use $wpdb->prepare() and cast numeric inputs with intval().
✔️Trade‑off – The fix adds a few extra milliseconds to each conversation fetch because of the extra preparation step. In high‑traffic chat rooms this can be noticeable, but the security gain outweighs the performance cost.
<?php
/**
* Plugin Name: Better Messages SQL Injection Patch (pre‑upgrade)
* Description: Wraps the conversation query in $wpdb->prepare() and forces integer casting.
*/
add_action( 'wp_ajax_bm_get_conversation', 'bm_secure_conversation_query' );
add_action( 'wp_ajax_nopriv_bm_get_conversation', 'bm_secure_conversation_query' );
function bm_secure_conversation_query() {
// Verify nonce first – the original plugin already does this, but we double‑check.
check_ajax_referer( 'bm_conversation_nonce', 'nonce' );
// Cast to integer to prevent injection.
$conversation_id = isset( $_GET['conversation_id'] )
? intval( $_GET['conversation_id'] )
: 0;
if ( $conversation_id <= 0 ) {
wp_send_json_error( [ 'message' => 'Invalid conversation ID' ] );
}
global $wpdb;
$table = $wpdb->prefix . 'bm_conversations';
$prepared_sql = $wpdb->prepare( "SELECT * FROM $table WHERE id = %d", $conversation_id );
$results = $wpdb->get_results( $prepared_sql );
if ( null === $results ) {
wp_send_json_error( [ 'message' => 'Database error' ] );
}
wp_send_json_success( $results );
}
5.5 How to Test
Open the browser console on a logged‑in user page that loads Better Messages.
Run the following fetch command to test the endpoint:
js
fetch('/wp-admin/admin-ajax.php?action=bm_get_conversation&conversation_id=1 UNION SELECT user_login,user_pass FROM wp_users--&nonce=YOUR_NONCE')
.then(r => r.text())
.then(console.log);
Expected result – The server returns a 400 Bad Request JSON error (Invalid conversation ID), not a data dump.
There was no currentusercan( 'manageoptions' ) check. Merely loading the page as any logged‑in user (including a subscriber) caused the $POST to be processed, because the form was submitted via JavaScript on page load.
6.2 Why It Matters
✔️Cost leakage – Turning the AI on triggers calls to OpenAI, Google Gemini, or other paid services. A malicious low‑privilege user could generate thousands of dollars in API usage.
✔️Data exposure – The AI may ingest site content (posts, comments) and send it to external endpoints, violating GDPR or HIPAA.
✔️Change: Added a capability filter (preupdateoptionopenstationaienabled) that checks manageoptions. Also added a nonce verification on the settings form.
✔️Trade‑off – The new filter introduces a tiny overhead (a single currentusercan() call) on every request that touches the openstationaienabled option, which is negligible.
<?php
/**
* Plugin Name: OpenStation Capability Bypass Patch (pre‑upgrade)
* Description: Prevents non‑admin users from toggling the AI feature.
*/
add_filter( 'pre_update_option_openstation_ai_enabled', function ( $new_value, $old_value, $option ) {
// Only admins (manage_options) may change this option.
if ( ! current_user_can( 'manage_options' ) ) {
// Log the attempt for forensic purposes.
if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
error_log(
sprintf(
'[OpenStation] Unauthorized AI toggle attempt by user %d',
get_current_user_id()
)
);
}
// Return the old value – the update is silently ignored.
return $old_value;
}
// Allow the change for authorized users.
return $new_value;
}, 10, 3 );
6.5 How to Test
Log in as a subscriber (or any role without manage_options).
Navigate to Settings → OpenStation → AI.
Toggle the switch and click “Save”.
Result – You should see a “You do not have permission to perform this action” message (the plugin may display a generic error). The option value in the database stays unchanged.
7. Customer Reviews for WooCommerce – Authorization Flaw (CVE‑2026‑89055)
7.1 What Happened
The AJAX endpoint ivolesavereviewmeta accepted a reviewid and a new rating value, then performed:
✔️Change: The handler now checks a nonce (ivolereviewnonce) and verifies that postauthor of the review matches getcurrentuserid().
✔️Trade‑off – Adding a nonce adds a tiny extra request (the nonce must be printed in the page markup). This is a negligible cost for the security gain.
Because $reason was not sanitized, an attacker could supply ../../wp-config.php and overwrite the core configuration file, gaining full site takeover.
8.2 Why It Matters
✔️Overwriting wp-config.php can inject malicious DB credentials or a backdoor.
✔️The altered config remains until manually fixed, providing persistence.
8.3 Official Fix
✔️Version: Bookly 28.3 (released 2024‑08‑05).
✔️Change: Whitelists allowed $reason values (rollback, debug) and runs sanitizefilename() before concatenation.
✔️Trade‑off – The whitelist restricts future custom reasons. If you need a new reason, extend the whitelist via a filter (booklyallowedrollback_reasons).
Store the actual patch files in a patches/ directory under version control. When you run composer install or composer update, Composer will apply the patches before the plugin is activated.
10.2 Nightly WP‑CLI Update Cron
Add a cron entry on the server (as the web‑user) that runs every night at 02:30:
The jq step will fail the job if any of the targeted CVEs are still present, enforcing a “no‑go‑live” rule until the patch is applied.
12. Post‑Patch Validation Checklist
After you have applied either the official update or the mu‑plugin workaround, run through this checklist on both staging and production:
Functional test – Verify the core feature of each plugin still works (e.g., submit a JetFormBuilder form, start a Better Messages chat, generate an OpenStation AI output).
Security test – Re‑run the same exploit attempts described in sections 4‑8. Confirm they now fail with a proper error.
Log review – Check wp-content/debug.log for any PHP Notice or Warning that could indicate a missing function or class.
Checksum verification – wp core verify-checksums and wp plugin verify-checksums. No mismatches should appear.
Performance baseline – Run ab -n 500 -c 20 External resource (or a similar load test) and compare response times before and after the patch. Document any increase > 5 % for later optimisation.
13. Long‑Term Maintenance Strategy
Goal
Recommended Practice
Frequency
------
----------------------
-----------
Stay ahead of new CVEs
Subscribe to WPScan RSS, NVD feeds, and vendor security mailing lists.
Daily
Automate patch application
Use Composer + cweagans/composer-patches + GitHub Actions to push patches automatically to staging.
On every PR merge
Reduce attack surface
Deactivate and delete any plugins/themes you do not actively use.
Quarterly
Hardening
Enable disable-file-mods in wp-config.php once you are confident in your deployment pipeline.
After stable release
Incident response drill
Simulate a breach (e.g., a successful XSS) and practice the rollback and forensic steps.
Twice a year
Example wp-config.php Hardening Flags
php
define( 'DISALLOW_FILE_EDIT', true ); // Prevents theme/plugin editor.
define( 'DISALLOW_FILE_MODS', true ); // Blocks plugin/theme install/updates via UI.
define( 'WP_AUTO_UPDATE_CORE', false ); // Force updates through CI only.
define( 'WP_DEBUG_DISPLAY', false ); // Never expose errors to visitors.
Note – When DISALLOWFILEMODS is true, you must rely on Composer/CLI for any future updates.
Can I wait for the next maintenance window to update the plugins?+
It is risky to wait. Each CVE allows either remote code execution or privilege escalation, so delaying more than a day leaves the site open to automated attacks.
Do mu‑plugin patches survive core or plugin upgrades?+
Yes. Mu‑plugins live in `wp-content/mu-plugins` and are loaded before regular plugins, so they remain untouched when WordPress or other plugins are updated.
How can I confirm the JetFormBuilder XSS fix works?+
After applying the `esc_html()` change, submit a form label containing `<script>` tags. The admin list should display the tags as plain text, not as executable JavaScript.
Is WPScan enough to catch future plugin CVEs?+
WPScan gives timely CVE alerts, but pairing it with static analysis tools like PHPStan helps catch insecure coding patterns before they are released.
What is the best way to lock plugin versions with Composer?+
Add exact version constraints in `composer.json`, e.g., `"jet-form-builder/jet-form-builder": "3.6.5.4"`, and use `cweagans/composer-patches` to apply community‑approved patches automatically.
The week's best on engineering, AI, and security — one email, no noise.
Read next
Shared topicssecurity·August 1, 2026
Mapster WP Maps vs Event Tickets: Which Authorization Bug Puts Your WordPress Site at Greater Risk
Event Tickets bug (CVE‑2026‑14822) lets anyone change an order’s status without any authentication. The flaw can trigger unauthorized refunds, cancel legitimate