CVE-2026-75816: WordPress Frontend Admin, unauthenticated RCE
Frontend Admin 3.29.11 skips edit_post when the object id is the string user_1. pre_update_value then wp_update_user's that user's email with no capability check. Unauthenticated form_submit. Password reset is the rest.
- Name
- CVE-2026-75816: WordPress Frontend Admin, unauthenticated RCE
- Type
- N-day analysis
- CVE
- CVE-2026-75816
- CVE Risk
- critical
- Disclosure Status
- public
- Vendor
- DynamiApps
- Affected
- Frontend Admin by DynamiApps (acf-frontend-form-element), lab on 3.29.11; 3.29.12 added the Email-field edit_user check; NVD lists through 3.29.12
- Published
- 19 Sept 2026
- Updated
- 19 Sept 2026
- Tags
- n-day, wordpress, auth-bypass, cwe-287, unauthenticated, rce
user_1 is not a post
I am @abraxas_null. The proof of concept is on GitHub: abraxas/CVE-2026-75816 (loopback client). The lab stack is lab/: Dockerfile, docker-compose.yml, docker-compose.override.yml. Authorized lab only. It talks to loopback.
CVE-2026-75816 (NVD, GHSA-pv54-wq7v-wf7v, Wordfence) is unauthenticated authentication bypass to account takeover in Frontend Admin by DynamiApps 3.29.11, slug acf-frontend-form-element. CWE-287. 9.8 Critical. NVD lists through 3.29.12. Changelog 3.29.12 added the Email-field edit_user check. The lab is 3.29.11. Current tree is 3.29.13.
The advisory names pre_update_value. That is a PHP method. HTTP is admin-ajax.php action=frontend_admin/form_submit, nopriv. The object id is the string user_1, not a numeric post. If you POST action=pre_update_value you get the theme back, or a 0, and nothing happens.
ActionPost::conditions_logic returns early when the id is not numeric. That skips current_user_can('edit_post'). Then acf_update_value on a user_email field with post id user_1 hits pre_update_value, which wp_update_users user 1's email with no capability check. WordPress password reset to that address is the rest. That is account takeover of an administrator. That is RCE on WordPress.
This is the map I used to get from a form 200 to an admin email that was not the admin's. Isolated lab, loopback only. I am not publishing a live password-reset against someone else's mailbox. The witness is the email string. The stack is in the CVE repo so you can run it at home. The client is the repo above.
What an attacker can do
Guest POST changes the administrator email. Password reset to that address is account takeover. The lab stops at the email change. I am not printing a lost-password recipe aimed at someone else's admin.
The lab (run this at home)
Source of truth is lab/ on GitHub. The Dockerfile pins wordpress:6.4-php8.2-apache. Compose adds mysql:8.0 and wordpress:cli. Port on loopback only. Bind 3.29.11 of the plugin next to compose as ./acf-frontend-form-element (SVN tag). Do not bind 3.29.12 if you want the sink.
# Loopback lab image pin for CVE-2026-75816. Full stack: docker-compose.yml
FROM wordpress:6.4-php8.2-apache# CVE-2026-75816 local WordPress lab. Loopback only.
services:
db:
image: mysql:8.0
environment:
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress
MYSQL_ROOT_PASSWORD: root
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"]
interval: 5s
timeout: 5s
retries: 30
start_period: 15s
wordpress:
image: wordpress:6.4-php8.2-apache
ports:
- "127.0.0.1:8088:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DEBUG: "1"
WORDPRESS_CONFIG_EXTRA: |
define('WP_DEBUG_LOG', '/var/log/lab/debug.log');
define('WP_DEBUG_DISPLAY', false);
volumes:
- wp_data:/var/www/html
- ./acf-frontend-form-element:/var/www/html/wp-content/plugins/acf-frontend-form-element:ro
depends_on:
db:
condition: service_healthy
wpcli:
image: wordpress:cli
user: "33:33"
volumes:
- wp_data:/var/www/html
- ./acf-frontend-form-element:/var/www/html/wp-content/plugins/acf-frontend-form-element:ro
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
WORDPRESS_DB_NAME: wordpress
depends_on:
wordpress:
condition: service_started
entrypoint: ["sleep", "infinity"]
volumes:
wp_data:docker-compose.override.yml only routes logs.
# App processes should log to stdout and/or /var/log/lab.
services:
db:
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
volumes:
- ./logs/db:/var/log/lab
- ./logs/db-mysql:/var/log/mysql
wordpress:
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
volumes:
- ./logs/wordpress:/var/log/lab
- ./logs/wordpress-apache:/var/log/apache2The CVE needs a published admin_form with who_can_see=all, post_id=user_1, a user_email field, a public page that renders that form, and a probe that prints user 1's email. Condensed fixture:
<?php
/**
* Lab fixture for Frontend Admin 3.29.11 (CVE-2026-75816).
* Public form, post_id=user_1, user_email field. Probe prints admin email.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action(
'template_redirect',
function () {
if ( empty( $_GET['fea_lab_email'] ) ) {
return;
}
$u = get_userdata( 1 );
header( 'Content-Type: text/plain; charset=utf-8' );
echo $u ? $u->user_email : '';
exit;
}
);
$form_id = wp_insert_post(
array(
'post_type' => 'admin_form',
'post_status' => 'publish',
'post_title' => 'lab email form',
'post_name' => 'lab-email-form',
'post_content' => serialize(
array(
'save_to_post' => 'edit_post',
'post_to_edit' => 'select_post',
'select_post' => 'user_1',
'post_id' => 'user_1',
'who_can_see' => 'all',
'form_conditions' => array(
array(
'who_can_see' => 'all',
'special_permissions' => array(),
'applies_to' => array( 'form' ),
),
),
)
),
),
true
);
update_post_meta( $form_id, 'form_key', 'form_lab75816' );
wp_insert_post(
array(
'post_type' => 'page',
'post_status' => 'publish',
'post_title' => 'fea lab',
'post_name' => 'fea-lab',
'post_content' => '[frontend_admin form="' . (int) $form_id . '"]',
),
true
);Bring-up from the CVE repo lab/:
git clone https://github.com/abraxas/CVE-2026-75816
cd CVE-2026-75816/lab
svn export https://plugins.svn.wordpress.org/acf-frontend-form-element/tags/3.29.11 acf-frontend-form-element
docker compose up -d --force-recreate
docker compose exec -T wpcli wp core install \
--url=http://127.0.0.1:8088 \
--title='CVE-2026-75816 Lab' \
--admin_user=admin \
--admin_password=labadmin \
--admin_email=lab@localhost.invalid \
--skip-email
docker compose exec -T wpcli wp plugin activate acf-frontend-form-element
docker compose cp seed.php wpcli:/tmp/seed.php
docker compose exec -T wpcli wp eval-file /tmp/seed.php
python3 ../CVE-2026-75816-Abraxas-Labs.pyThe seed is the fixture above; it is not in lab/ on GitHub. The full seed also inserts the acf-field of type user_email. Do not publish the port off loopback. The proof of concept is written for 127.0.0.1:8088.
What the tree actually registers
Submit_Form registers wp_ajax_nopriv_frontend_admin/form_submit. Unauthenticated. The form page carries _acf_form, _acf_nonce, _acf_objects (encrypted). Harvest those like a visitor. POST acff[post][<field_key>] with the new email.
ActionPost::conditions_logic is the skip:
public function conditions_logic( $settings, $condition, $user ){
$post_id = $settings['post_id'] ?? 'none';
if ( ! is_numeric( $post_id ) ) {
return $settings;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
// ...
$settings['post_id'] = 'none';
}
return $settings;
}user_1 is not numeric. The edit_post gate never runs. ActionPost::run still acf_update_values the field against that id.
Then user_email::pre_update_value in 3.29.11:
function pre_update_value( $checked, $value, $post_id, $field ) {
if ( $this->name !== $field['type'] ) {
return $checked;
}
$user = explode( 'user_', $post_id );
if ( ! empty( $user[1] ) ) {
$user_id = $user[1];
wp_update_user(
array(
'ID' => $user_id,
'user_email' => esc_attr( $value ),
)
);
}
return true;
}No current_user_can('edit_user'). esc_attr is not authorization. User 1's email becomes whatever was in the POST.
3.29.12 added that capability check on the Email field (changeset 3664865). That is the patch. Update. NVD still says through 3.29.12; the lab is 3.29.11 because that is the last tree where this path is open.
The 200 that was a form
The first client I pointed at this was polite. It used action=pre_update_value or parse_array. Function names in the advisory are PHP methods. You get 0 from admin-ajax.php, or the theme. The email is still lab@localhost.invalid.
A few other ways to lose without learning anything:
- Hard-coded nonce. Harvest
_acf_nonceand_acf_objectsfrom/fea-lab/. - Numeric
post_id. Thenedit_postactually runs and a guest is denied. who_can_seenotall. The form never renders for uid 0.- 3.29.12. Email field now asks
edit_user. - A reverse shell. Theatre. The witness is the email string.
- Actually firing lost-password against a real mailbox. The lab stops at the email change.
The tell is small JSON from ajax: {"success":true,... "success_message":"Post updated"}. Then GET /?fea_lab_email=1 is the new address. Theme HTML is a miss.
What I actually did
Treat the on-disk product as the spec. Wordfence named pre_update_value and user_1. I read conditions_logic, then the Email field, then harvested the public form. Proof of concept: CVE-2026-75816-Abraxas-Labs.py.
Harvest, POST, probe. GET /fea-lab/. Pull the hidden fields and the user_email field key. POST frontend_admin/form_submit with acff[post][<key>]=pocwitness75816@localhost.invalid. GET /?fea_lab_email=1.
Witness in the body. That address. If user 1's email changed, wp_update_user ran without a logged-in editor. Password reset to that address is how you take the account. I am not going to print a lost-password recipe aimed at someone else's admin.
Last lab run, trimmed:
email_before=lab@localhost.invalid
harvest status=200
ajax status=200
{"success":true,"data":{"success_message":"Post updated"}}
email_after=pocwitness75816@localhost.invalid
SUCCESS CVE-2026-75816Small JSON. Then the email. That is the whole argument. The client that produced it is on GitHub.
Wrong turns: action=pre_update_value or parse_array (function names are not routes); hard-coded nonce; numeric post_id (then edit_post actually runs and a guest is denied); who_can_see not all; 3.29.12; treating lab@localhost.invalid still in the body as success.
What this is not
It is not "ACF updates user email." ACF still expects a capable user. This plugin's post-action treats user_1 as a post id, skips the post cap because it is not numeric, then the Email field writes the user anyway.
Update to 3.29.13 (or at least 3.29.12). Re-run the loopback client against the patched build: the witness must not appear.
I am not going to print a multipart recipe you can paste at someone else's form_submit. The ! is_numeric skip and the missing edit_user check are the useful part. If you own the box, run the proof of concept against loopback.
Client bugs look like product bugs. A helper that shadows http.client never sends a packet. .recv() on an HTTPResponse is not .read(). I mention that once because it cost time and looked, for a minute, like the plugin was fine.
References
- Proof of concept: abraxas/CVE-2026-75816 · CVE-2026-75816-Abraxas-Labs.py
- Lab:
lab/· Dockerfile · docker-compose.yml · override - @abraxas_null · github.com/abraxas · abraxaslabs.tech · abraxas.null@proton.me
- CVE-2026-75816 · NVD · GHSA-pv54-wq7v-wf7v · Wordfence · CWE-287
- Trac 3.29.11:
pre_update_value,conditions_logic,ActionPost::run, nopriv form_submit - Patch: changeset 3664865
- Source: SVN tags · Trac browser