Automated Threat Modeling

Brief Summary: BuddyForms CVE-2026-107622, Unauthenticated Privilege Escalation via bf_post_type Form-Type Confusion

A brief summary of CVE-2026-107622, an unauthenticated privilege escalation in the BuddyForms WordPress plugin through version 2.10.5, including root cause analysis and patch details for version 2.10.6.

CVE Analysis

9 min read

Gaurav Sarraf
Gaurav Sarraf

2026-11-06

Brief Summary: BuddyForms CVE-2026-107622, Unauthenticated Privilege Escalation via bf_post_type Form-Type Confusion
Experimental AI-Generated Content

This CVE analysis is an experimental publication that is completely AI-generated. The content may contain errors or inaccuracies and is subject to change as more information becomes available. We are continuously refining our process.

If you have feedback, questions, or notice any errors, please reach out to us.

[email protected]

Introduction

A form plugin designed to let site members submit articles and listings without touching the WordPress admin area ended up letting anonymous visitors hand themselves administrator accounts. CVE-2026-107622 affects Themekraft's BuddyForms, where the boundary between public frontend content submission and privileged form configuration collapses. An unauthenticated attacker can inject a rogue registration form and register as an administrator.

BuddyForms is a frontend content management plugin for WordPress. Themekraft markets it as a tool for publishing user-generated content such as articles, portfolios, and listings, rather than as a traditional contact form product. It integrates with BuddyPress, Ultimate Member, WooCommerce, BuddyBoss, and custom post type tooling, and reports between 800 (WordPress.org) and 900 (WPScan) active installations. The intended design keeps member-facing submission separate from administrative control, and that separation is exactly what this vulnerability breaks.

Technical information

The vulnerability is classified as CWE-269, Improper Privilege Management, and carries a CVSS 3.1 score of 9.8 with the vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H. The root cause is a chain of authorization gaps rather than a single bug. Together, those gaps turn the permission to submit ordinary frontend content into the ability to create and persist a privileged BuddyForms configuration object.

Root cause: authorization is decoupled from the object being created

The public frontend submission handler buddyforms_form_response_no_ajax() authorizes submissions from logged-out visitors based solely on the target form's public_submit flag. It then independently trusts the attacker-supplied $_POST['bf_post_type'] value to decide what type of object the submission creates. The authorization decision (is this form publicly submittable?) is therefore completely separated from the object that actually receives the submitted data.

A cross-check was introduced in version 2.10.2 to validate the post type, but it only runs for existing posts, that is, when post_id != 0. During new-object creation the check is entirely bypassed. The vulnerable existing-post logic looked like this:

$target_post = get_post( $post_id ); $expected_post_type = $post_type; if ( ! empty( $target_post ) && $target_post->post_status !== 'auto-draft' && ! empty( $buddyforms[ $form_slug ]['post_type'] ) ) { $expected_post_type = $buddyforms[ $form_slug ]['post_type']; } if ( empty( $target_post ) || $target_post->post_type !== $expected_post_type ) {

Because this only protects edits to existing posts, an attacker who creates a new object of type buddyforms (the plugin's own form configuration custom post type) sails straight past it.

The second gap: unauthorized persistence of form options

The second half of the chain is the save_post listener buddyforms_edit_form_save_meta_box_data(). For a newly inserted buddyforms CPT post, it unconditionally persists attacker-supplied $_POST['buddyforms_options'], with no current_user_can() capability check and no form-builder nonce. An unauthenticated request can therefore write an entire form configuration into the live global form registry, with no authorization or request integrity verification.

Attack flow

Putting the two gaps together, the documented exploitation chain proceeds as follows:

  1. The attacker locates a page hosting a public BuddyForms form, where public_submit is enabled. This satisfies the frontend handler's authorization requirement.
  2. The attacker submits to buddyforms_form_response_no_ajax() while logged out, setting $_POST['bf_post_type'] to buddyforms. Since the 2.10.2 cross-check applies only to existing posts, the new-object creation path accepts the attacker-controlled type and creates a new buddyforms configuration object.
  3. The save_post listener buddyforms_edit_form_save_meta_box_data() fires on that insertion and, with no capability check and no nonce, persists the attacker-supplied $_POST['buddyforms_options'] into the global form registry. The attacker's payload defines a registration form whose new_user_role is administrator.
  4. The attacker submits a registration through that rogue form. Because the configuration specifies an administrator-level new_user_role, a new WordPress account is created with administrator privileges, and the site is fully taken over.

Exploitation requires two conditions, and both are standard BuddyForms deployment configurations: the WordPress site must have "Anyone can register" enabled, and at least one public BuddyForms form must be attached to a page.

Patch information

Themekraft fixed this in BuddyForms 2.10.6, and Wordfence lists 2.10.6 as the patched version. The upstream change is GitHub PR #779, titled "fix: take the submission post type from the form and protect form saves." It was merged into develop on 19 October 2026 as merge commit 3b5c843f3fc03f3a2e273a3e039f1361fc04f607, touches 6 files (+475 / -27), and was pushed to the WordPress.org plugin repository as SVN changeset 3780268. The 2.10.6 changelog entry, dated 19 October 2026, lists the same three security changes.

Because the exploit needed several separate weaknesses to work together, the patch fixes each one independently, so that even if one check were bypassed later, the others should still stop the attack.

1. The post type comes from the saved form, not the request

In includes/form/form-control.php, buddyforms_process_submission() previously let the client-supplied bf_post_type override the form's configured post type. The patch deletes that override:

- if ( isset( $_POST['bf_post_type'] ) ) { - $post_type = sanitize_text_field( wp_unslash( $_POST['bf_post_type'] ) ); - }

The handler now derives the post type early from the stored form settings, and refuses certain post types outright:

+ // The post type always comes from the saved form. The request's bf_post_type (and post_type) is + // ignored, so a submission can never create or edit posts of another type, forms included. + $post_type = buddyforms_get_form_post_type( $form_slug ); + if ( buddyforms_is_submission_post_type_blocked( $post_type ) ) { + return array( + 'hasError' => true, + 'form_slug' => $form_slug, + 'error_message' => __( 'Form submit error. Please contact the site administrator.', 'buddyforms' ), + ); + }

Two new helpers support this. buddyforms_get_form_post_type() reads only $buddyforms[ $form_slug ]['post_type'] and is adjustable via the new buddyforms_submission_post_type filter. buddyforms_is_submission_post_type_blocked() is a deny-list of internal types that a public submission must never write: buddyforms, customize_changeset, nav_menu_item, user_request, oembed_cache, revision, wp_template, wp_template_part, wp_global_styles, wp_navigation, and wp_block. It can be extended with the buddyforms_submission_blocked_post_types filter.

The existing-post check that 2.10.2 added is also simplified so that every target post, auto-drafts included, must match the form's post type:

- $target_post = get_post( $post_id ); - $expected_post_type = $post_type; - if ( ! empty( $target_post ) && $target_post->post_status !== 'auto-draft' && ! empty( $buddyforms[ $form_slug ]['post_type'] ) ) { - $expected_post_type = $buddyforms[ $form_slug ]['post_type']; - } - if ( empty( $target_post ) || $target_post->post_type !== $expected_post_type ) { + // The post to edit, auto-drafts included, must be of the form's post type. + $target_post = get_post( $post_id ); + if ( empty( $target_post ) || $target_post->post_type !== $post_type ) {

2. Form-builder saves need a nonce and edit_post

In includes/admin/register-post-types.php, the save_post listener buddyforms_edit_form_save_meta_box_data() previously saved $_POST['buddyforms_options'] for any new buddyforms post with no capability check and no nonce. The patch adds a gate function, buddyforms_is_form_builder_save(), which passes only when both of these conditions hold: the request carries a valid nonce tied to that specific form ID (buddyforms_form_builder_save_{ID}), and the current user satisfies current_user_can( 'edit_post', $post_id ). The listener now exits early if the gate fails:

+ if ( ! buddyforms_is_form_builder_save( $post_id ) ) { + return; + }

The nonce is printed by a new edit_form_top hook, buddyforms_form_builder_nonce_field(), rather than inside the Form Builder meta box, because loading a template replaces the meta box markup and would otherwise remove the nonce. The same gate also protects the slug-rename path in buddyforms_wp_insert_post_data().

The patch also limits what a save can change. A new list, buddyforms_form_protected_options(), names the settings that control public access: form_type, public_submit, public_submit_create_account, and registration. If a save occurs outside wp-admin, or without a logged-in user, these keys are reset to their stored values or removed.

3. Anonymous registration cannot hand out admin-level roles

The final step of the attack was registering through a rogue form whose new_user_role was administrator. In includes/wp-insert-user.php, a new function, buddyforms_is_registration_role_allowed(), refuses any role that has one of these capabilities: manage_options, promote_users, edit_users, create_users, or install_plugins. The list is adjustable with the buddyforms_registration_blocked_capabilities filter. The check runs before wp_insert_user() for logged-out visitors:

+ // A visitor can never register with a role that can manage the site, whatever the form says. + if ( ! $hasError && ! is_user_logged_in() && ! buddyforms_is_registration_role_allowed( $user_role, $form_slug ) ) { + $hasError = true;

This check stands on its own, so a rogue or misconfigured form that still reached the registry could not create an administrator.

In includes/admin/form-metabox.php, the entry-fields meta box now prints a per-post nonce, and its save handler skips autosaves and requires both a valid nonce and edit_post. In includes/admin/form-builder/meta-boxes/metabox-select-form.php, the old permission check depended on the client-supplied $_POST['post_type'] to choose between edit_page and edit_post. The patch replaces it with a single edit_post check (WordPress maps edit_post to edit_page for pages), and the handler returns early if _bf_form_slug is missing.

5. Behaviour changes and robustness fixes

For integrators, the post_type attribute of the [buddyforms_form] shortcode no longer changes the post type that entries are saved as. Use the buddyforms_submission_post_type filter instead. The PR also fixes a separate bug. A new post submitted with post_id 0 now gets its ID back from buddyforms_update_post(), and the error path calls get_error_message() only when it actually has a WP_Error, which resolves a fatal error when a submission arrived without an entry ID.

Regression coverage

The PR adds a WP-CLI test, tests/security/form-type-confusion.php, which runs each scenario in a separate process. It covers guest submissions with bf_post_type=buddyforms and bf_post_type=customize_changeset, taking over a buddyforms auto-draft, guest registration with the administrator role, and builder saves with no nonce, a subscriber's nonce, or another form's nonce. It also verifies that normal guest submission and subscriber registration still work. According to the PR description, all 17 cases pass on the fix branch, while only 4 of 17 pass on the old develop branch.

Affected systems and versions

  • Affected product: BuddyForms plugin for WordPress by Themekraft.
  • Affected versions: All versions up to and including 2.10.5.
  • Patched version: 2.10.6.
  • Exploitation prerequisites: The WordPress site must have "Anyone can register" enabled, and at least one public BuddyForms form (with public_submit) must be attached to a page. Both are standard BuddyForms deployment configurations.

The release information has one discrepancy. The WordPress plugin directory page displayed 2.10.5 at the time of review, which is an affected version. That directory snapshot does not supersede the advisory's guidance to update to 2.10.6 or a newer patched release.

Vendor security history

BuddyForms has a documented history of authorization-related vulnerabilities, which provides context for this issue without implying the vendor is uniquely insecure:

Historical vulnerabilityDocumented impact and accessAffected versionsFixed version
CVE-2024-8246Privilege escalation; Contributor privileges requiredThrough 2.8.112.8.12
CVE-2024-32830Unauthenticated arbitrary file read and server-side request forgeryThrough 2.8.82.8.9
CVE-2026-118100Unauthenticated information exposure (form configuration disclosure)Through 2.10.22.10.3

These earlier cases reach their outcomes through different mechanisms and starting privileges. CVE-2024-8246 required an authenticated Contributor account, whereas the current CVE requires no account at all. Keep their fixed versions separate: the remediation for CVE-2026-107622 is specifically 2.10.6 or newer, and an older historical fix must not be mistaken for protection against this issue.

References

Detect & fix
what others miss

Works with
  • GitHub
  • GitLab
  • Bitbucket
  • Azure DevOps Services
  • Gerrit
  • CVS
  • Jira
  • Linear
  • Slack
  • Security Compass
Explore integrations
Security magnifying glass visualization