CVE-2026-32408

Brizy <= 2.7.23 - Missing Authorization

mediumMissing Authorization
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
2.7.24
Patched in
53d
Time to patch

Description

The Brizy plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in versions up to, and including, 2.7.23. This makes it possible for authenticated attackers, with contributor-level access and above, to perform an unauthorized action.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=2.7.23
PublishedFebruary 22, 2026
Last updatedApril 15, 2026
Affected pluginbrizy

What Changed in the Fix

Changes introduced in v2.7.24

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-32408 (Brizy <= 2.7.23) ## 1. Vulnerability Summary The Brizy Page Builder plugin for WordPress is vulnerable to **Missing Authorization** in several AJAX API endpoints. The plugin implements an abstract API class `Brizy_Admin_AbstractApi` and multiple subcla…

Show full research plan

Exploitation Research Plan - CVE-2026-32408 (Brizy <= 2.7.23)

1. Vulnerability Summary

The Brizy Page Builder plugin for WordPress is vulnerable to Missing Authorization in several AJAX API endpoints. The plugin implements an abstract API class Brizy_Admin_AbstractApi and multiple subclasses (e.g., Brizy_Admin_Blocks_Api, Brizy_Admin_Fonts_Api) that register AJAX actions. While these actions verify a WordPress nonce and an editor version string, they fail to perform any capability checks (e.g., current_user_can()). This allows authenticated users with Contributor-level access and above to perform administrative actions such as creating, updating, or deleting global blocks, layouts, and custom fonts.

2. Attack Vector Analysis

  • Endpoint: wp-admin/admin-ajax.php
  • AJAX Action: brizy-delete-saved-block (or any other action registered in the Brizy_Admin_*_Api classes).
  • HTTP Method: POST
  • Payload Parameters:
    • action: brizy-delete-saved-block (derived from Brizy_Editor::prefix() + self::DELETE_SAVED_BLOCK_ACTION)
    • hash: A valid WordPress nonce for the brizy-api action.
    • version: The current plugin version (e.g., 2.7.23).
    • uid: The unique identifier of the block to delete.
  • Required Authentication: Contributor level (PR:L).
  • Preconditions: The attacker must be logged in as a Contributor and have access to the Brizy editor interface (which Contributors have for their own posts) to retrieve the required nonce.

3. Code Flow

  1. Entry Point: The AJAX action is registered in admin/blocks/api.php via initializeApiActions():
    $pref = 'wp_ajax_' . Brizy_Editor::prefix(); // prefix is 'brizy'
    add_action( $pref . self::DELETE_SAVED_BLOCK_ACTION, array( $this, 'actionDeleteSavedBlock' ) );
    
  2. Abstract Verification: The handler calls verifyNonce( self::nonce ) defined in admin/abstract-api.php:
    protected function verifyNonce( $action ) {
        $version = $this->param( 'version' );
        if ( $version !== BRIZY_EDITOR_VERSION ) { ... } // Checks version match
        $this->checkNonce( $action ); // Verifies 'brizy-api' nonce
    }
    
  3. Missing Sink: After verifyNonce, the code proceeds directly to the functional logic (e.g., deleting a block via Brizy_Admin_Blocks_Manager) without any current_user_can() check.

4. Nonce Acquisition Strategy

The brizy-api nonce is required for all API calls. It is localized for the Brizy editor.

  1. Shortcode: Brizy uses a custom editor interface. Creating a post and clicking "Edit with Brizy" loads the necessary scripts.
  2. Page Creation: Use WP-CLI to create a post the Contributor can edit.
    wp post create --post_type=post --post_title="Exploit Page" --post_status=publish --post_author=[CONTRIBUTOR_ID]
    
  3. Navigation: Navigate to the Brizy editor for that post. The URL format is typically: wp-admin/post.php?post=[ID]&action=brizy-edit.
  4. Extraction: Use browser_eval to extract the configuration object. Brizy typically stores its configuration in a global JavaScript object named Brizy or BrizyLocal.
    • Target Variable: window.brizyEditorConfig?.api?.nonce or window.Brizy?.config?.api?.nonce.
    • Version: The version string is also usually available in the same config object: window.brizyEditorConfig?.version.

5. Exploitation Strategy

We will demonstrate the vulnerability by deleting a "Saved Block" created by an Administrator.

  1. Retrieve Target UID: Use WP-CLI to find a block UID created by the Admin.
    • Brizy saved blocks are usually in the brizy_saved_block post type.
    • wp post list --post_type=brizy_saved_block --format=ids
    • Get the UID from post meta: wp post meta get [POST_ID] brizy_post_uid
  2. Acquire Nonce: As a Contributor, visit the editor and extract the hash (nonce) and version.
  3. Execute Exploit: Send a POST request to admin-ajax.php.

Request Details:

  • URL: http://[TARGET]/wp-admin/admin-ajax.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    action=brizy-delete-saved-block&hash=[NONCE]&version=[VERSION]&uid=[TARGET_UID]
    

6. Test Data Setup

  1. Administrator Actions:
    • Create a saved block in Brizy (this generates a brizy_saved_block post).
    • Record the brizy_post_uid of this block.
  2. Contributor User:
    • Create a user with the contributor role.
  3. Editor Access:
    • Ensure Brizy is enabled for the post post type.
    • Create a post authored by the Contributor.

7. Expected Results

  • The AJAX request should return a JSON success response: {"success": true, "data": []}.
  • The brizy_saved_block post associated with the target UID should be deleted or moved to trash.
  • Unauthorized users (Contributors) can manipulate global Brizy entities they do not own.

8. Verification Steps

  1. Check Post Existence: Use WP-CLI to verify the block is gone.
    wp post list --post_type=brizy_saved_block --post_author=[ADMIN_ID]
    
  2. Check Database: Verify the post with the specific UID no longer exists in wp_posts or wp_postmeta.

9. Alternative Approaches

If delete-saved-block fails, try brizy-create-font in Brizy_Admin_Fonts_Api:

  • Action: brizy-create-font
  • Payload: Requires id, family, and fonts[] file upload.
  • Impact: Allows a Contributor to upload arbitrary files to the wp-content/uploads/brizy/fonts directory, which could lead to further exploitation depending on file handling.

Another alternative is brizy-upload-blocks in Brizy_Admin_Blocks_Api:

  • Action: brizy-upload-blocks
  • Payload: Requires a ZIP file containing block data.
  • Impact: Allows mass creation of global blocks and potentially file uploads via the ZIP extraction process.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Brizy Page Builder plugin for WordPress is vulnerable to unauthorized access due to missing capability checks in several AJAX API endpoints. This allows authenticated attackers with contributor-level access and above to perform administrative actions such as creating, updating, or deleting global blocks, layouts, and custom fonts.

Vulnerable Code

// admin/abstract-api.php line 24
	protected function verifyNonce( $action ) {

		$version = $this->param( 'version' );
		if ( $version !== BRIZY_EDITOR_VERSION ) {
			Brizy_Logger::instance()->critical( 'Request with invalid editor version',
				[
					editorVersion'   => BRIZY_EDITOR_VERSION,
					'providedVersion' => $version
				] );

			$this->error( 400, "Invalid editor version. Please refresh the page and try again" );
		}

		$this->checkNonce( $action );
	}

---

// admin/blocks/api.php line 577
	public function actionDeleteSavedBlock() {
		$this->verifyNonce( self::nonce );
		if ( ! current_user_can( 'edit_pages' ) ) {
			$this->error( 403, 'Unauthorized.' );
		}
		if ( ! $this->param( 'uid' ) ) {
			$this->error( 400, 'Invalid uid' );
		}

		$bockManager = new Brizy_Admin_Blocks_Manager( Brizy_Admin_Blocks_Main::CP_SAVED );

		try {
			$bockManager->deleteEntity( $this->param( 'uid' ) );
		} catch ( Exception $exception ) {
			$this->error( 400, $exception->getMessage() );
		}

		$this->success( [] );
	}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/brizy/2.7.23/admin/abstract-api.php /home/deploy/wp-safety.org/data/plugin-versions/brizy/2.7.24/admin/abstract-api.php
--- /home/deploy/wp-safety.org/data/plugin-versions/brizy/2.7.23/admin/abstract-api.php	2024-09-17 07:42:50.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/brizy/2.7.24/admin/abstract-api.php	2026-02-10 07:58:30.000000000 +0000
@@ -21,7 +21,14 @@
 	/**
 	 * @param $action
 	 */
-	protected function verifyNonce( $action ) {
+	protected function verifyUserCanEdit() {
+		if (!Brizy_Editor_User::is_user_allowed($this->param('post'))) {
+			$this->error( 400, "You are not allowed to edit this post" );
+		}
+	}
+	protected function verifyAuthorization( $action ) {
+
+		$this->verifyUserCanEdit();
 
 		$version = $this->param( 'version' );
 		if ( $version !== BRIZY_EDITOR_VERSION ) {
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/brizy/2.7.23/admin/blocks/api.php /home/deploy/wp-safety.org/data/plugin-versions/brizy/2.7.24/admin/blocks/api.php
--- /home/deploy/wp-safety.org/data/plugin-versions/brizy/2.7.23/admin/blocks/api.php	2025-07-17 12:40:20.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/brizy/2.7.24/admin/blocks/api.php	2026-02-10 07:58:30.000000000 +0000
@@ -73,7 +73,7 @@
 	}
 
 	public function actionDownloadBlocks() {
-		$this->verifyNonce( self::nonce );
+		$this->verifyAuthorization( self::nonce );
 		if ( ! $this->param( 'uid' ) ) {
 			$this->error( 400, 'Invalid block uid param' );
 		}
@@ -125,7 +125,7 @@
 
 	public function actionUploadBlocks() {
 		try {
-			$this->verifyNonce( self::nonce );
+			$this->verifyAuthorization( self::nonce );
 			if ( ! isset( $_FILES['files'] ) ) {
 				$this->error( 400, __( 'Invalid block file' ) );
 			}
@@ -164,7 +164,7 @@
 	}
 
 	public function actionGetGlobalBlocks() {
-		$this->verifyNonce( self::nonce );
+		$this->verifyAuthorization( self::nonce );
 		try {
 			$fields      = $this->param( 'fields' ) ? $this->param( 'fields' ) : [];
 			$bockManager = new Brizy_Admin_Blocks_Manager( Brizy_Admin_Blocks_Main::CP_GLOBAL );
@@ -180,7 +180,7 @@
 	}
 
 	public function actionCreateGlobalBlock() {
-		$this->verifyNonce( self::nonce );
+		$this->verifyAuthorization( self::nonce );
 		if ( ! $this->param( 'uid' ) ) {
 			$this->error( 400, 'Invalid uid' );
 		}
@@ -256,7 +256,7 @@
 	}
 
 	public function actionUpdateGlobalBlock() {
-		$this->verifyNonce( self::nonce );
+		$this->verifyAuthorization( self::nonce );
 		try {
 
 			if ( ! $this->param( 'uid' ) ) {
@@ -343,7 +343,7 @@
 	}
 
 	public function actionUpdateGlobalBlocks() {
-		$this->verifyNonce( self::nonce );
+		$this->verifyAuthorization( self::nonce );
 		try {
 
 			if ( ! current_user_can( 'edit_pages' ) ) {
@@ -436,7 +436,7 @@
 	}
 
 	public function actionDeleteGlobalBlock() {
-		$this->verifyNonce( self::nonce );
+		$this->verifyAuthorization( self::nonce );
 		if ( ! current_user_can( 'edit_pages' ) ) {
 			$this->error( 403, 'Unauthorized.' );
 		}
@@ -454,7 +454,7 @@
 	}
 
 	public function actionGetSavedBlocks() {
-		$this->verifyNonce( self::nonce );
+		$this->verifyAuthorization( self::nonce );
 		try {
 			$fields      = $this->param( 'fields' ) ? $this->param( 'fields' ) : [];
 			$bockManager = new Brizy_Admin_Blocks_Manager( Brizy_Admin_Blocks_Main::CP_SAVED );
@@ -472,7 +472,7 @@
 	}
 
 	public function actionGetSavedBlockByUid() {
-		$this->verifyNonce( self::nonce );
+		$this->verifyAuthorization( self::nonce );
 		if ( ! $this->param( 'uid' ) ) {
 			$this->error( 400, 'Invalid uid' );
 		}
@@ -492,7 +492,7 @@
 	}
 
 	public function actionCreateSavedBlock() {
-		$this->verifyNonce( self::nonce );
+		$this->verifyAuthorization( self::nonce );
 		if ( ! current_user_can( 'edit_pages' ) ) {
 			$this->error( 403, 'Unauthorized.' );
 		}
@@ -532,7 +532,7 @@
 	}
 
 	public function actionUpdateSavedBlock() {
-		$this->verifyNonce( self::nonce );
+		$this->verifyAuthorization( self::nonce );
 		if ( ! current_user_can( 'edit_pages' ) ) {
 			$this->error( 403, 'Unauthorized.' );
 		}
@@ -578,7 +578,7 @@
 	}
 
 	public function actionDeleteSavedBlock() {
-		$this->verifyNonce( self::nonce );
+		$this->verifyAuthorization( self::nonce );
 		if ( ! current_user_can( 'edit_pages' ) ) {
 			$this->error( 403, 'Unauthorized.' );
 		}
@@ -604,7 +604,7 @@
 	public function actionUpdateBlockPositions() {
 
 		global $wpdb;
-		$this->verifyNonce( self::nonce );
+		$this->verifyAuthorization( self::nonce );
 		if ( ! current_user_can( 'edit_pages' ) ) {
 			$this->error( 403, 'Unauthorized.' );
 		}

Exploit Outline

The exploit targets the AJAX endpoints registered by the Brizy plugin which fail to implement authorization checks. 1. Log in as a user with Contributor-level permissions. 2. Navigate to the Brizy editor for a post the contributor is allowed to edit (e.g., /wp-admin/post.php?post=[ID]&action=brizy-edit) to obtain a valid WordPress nonce for the 'brizy-api' action and the current BRIZY_EDITOR_VERSION string from the localized JavaScript configuration (window.brizyEditorConfig). 3. Identify a target object UID (such as a Global Block, Saved Block, or Font UID) that should be restricted to administrators. 4. Send a POST request to /wp-admin/admin-ajax.php with the following parameters: - action: The target Brizy action (e.g., brizy-delete-saved-block or brizy-create-font). - hash: The extracted 'brizy-api' nonce. - version: The current Brizy plugin version. - uid: The identifier of the block or object to manipulate. 5. Because the vulnerable code only verifies the nonce and version but lacks capability checks (current_user_can), the server will execute the administrative action on behalf of the low-privileged user.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.