September 7, 2026
CVE-2026โ11991 Case Study FlowForms <= 1.1.1
๏ทฝ

By phantom_hat
4 min read
CVE-2026โ11991 Case Study FlowForms <= 1.1.1 โ Insecure Direct Object Reference Authenticated (Contributor+) Form Publishing
๏ทฝ
Introduction
This case study focuses on CVE-2026โ11991 Discovered By Phantom Hat, an Unauthorized Form Publication vulnerability discovered in FlowForms, a WordPress form builder plugin. The vulnerability carries a CVSS score of 6.4 MEDIUM (AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N) and affects all versions โค 1.1.1.
The vulnerability is rooted in Authorization Bypass Through User-Controlled Key (CWE-639) and exists in the plugin's REST API publish endpoint:
POST /index.php?rest_route=/flowforms/v1/forms/{id}/publishPOST /index.php?rest_route=/flowforms/v1/forms/{id}/publishIn WordPress, the Contributor role is intentionally restricted from publishing content this is a core editorial workflow control. FlowForms' /publish endpoint bypasses this model entirely by only checking for the edit_posts capability, which Contributors possess. This allows any authenticated (Contributor+) low-privileged user to publish any form directly to the live site, completely bypassing editorial review.
In this case study I will demonstrate the Target Overview, Source Code Discovery & Analysis, the exact capability misconfiguration that creates the privilege escalation primitive, Manual Exploitation with a working Proof of Concept, and a deep Patch Diffing of the fix.
Target Overview
FlowForms (by Priyanshu Chaudhary) is a self-hosted, open-source conversational form builder plugin for WordPress. Unlike traditional multi-field contact form plugins, FlowForms delivers a Typeform-style step-by-step experience one question at a time aimed at improving form completion rates and user engagement.
The plugin ships with a React-powered drag-and-drop builder supporting 8 field types (Short Text, Long Text, Multiple Choice, Checkboxes, Rating, Yes/No, Email, and Number), full design customisation, pre-built templates, and flexible embed options via Shortcode, Gutenberg Block, or direct full-page URL (/flowform/{id}).
From a security perspective, FlowForms exposes a REST API registered under the flowforms/v1 namespace, providing endpoints for form lifecycle management including:
POST /flowforms/v1/formscreate a new formPOST /flowforms/v1/forms/{id}/publishpublish a form to the live site
The /publish endpoint is the attack surface. It accepts a user-supplied form {id} in the URL path and is responsible for Publishing the form. As we will demonstrate, this endpoint performs an insufficient capability check, and the form ID being entirely user-controlled leads to authorization bypass.
Discovery / Analysis
The vulnerability originates in the file:
wp-content/plugins/flowforms/includes/class-rest-api.phpwp-content/plugins/flowforms/includes/class-rest-api.phpIn the start we get a function.
public function register_routes()
{
$ns = 'flowforms/v1';
// SNIP
}public function register_routes()
{
$ns = 'flowforms/v1';
// SNIP
}at the start of this function there's a namespace for the Rest API.
// POST publish form (promote draft โ published, clear draft)
register_rest_route($ns, '/forms/(?P<id>\d+)/publish', [
'methods' => 'POST',
'callback' => [$this, 'publish_form'],
'permission_callback' => fn() => current_user_can('edit_posts'),
]); // POST publish form (promote draft โ published, clear draft)
register_rest_route($ns, '/forms/(?P<id>\d+)/publish', [
'methods' => 'POST',
'callback' => [$this, 'publish_form'],
'permission_callback' => fn() => current_user_can('edit_posts'),
]);then it register a rest API route to publish forms using the wordpress register_rest_routes() funtction:
register_rest_route( string $route_namespace, string $route, array $args = array(), bool $override = false ): boolregister_rest_route( string $route_namespace, string $route, array $args = array(), bool $override = false ): boolwhile registering routes there is a problem which exists in permission_callback it checks only the user has capability using current_user_can() to edit posts or not, it doesn't check is user authorized to modify or publish that post. that's the vulnerability.
and as a callback function it uses publish_form function in the class to publish the form. Below is the syntax of current_user_can which can check the capability on the specific post too.
current_user_can( string $capability, mixed $args ): boolcurrent_user_can( string $capability, mixed $args ): boolif we provide the form id it it will check the capability on that form.
publish_form Logic
public function publish_form($request)
{
$form_id = absint($request['id']);
// SNIP
// Promote draft โ published, clear draft. Design is untouched (top-level).
$json = $this->encode_slots($slots['content']['draft'], null, $slots['design'], $slots['settings']);
$saved = $this->save_post_content($form_id, $json);
if (! $saved) {
return new WP_REST_Response(['message' => 'Failed to publish form.'], 500);
}
return new WP_REST_Response([
'success' => true,
'form_id' => $form_id,
'message' => 'Form published successfully.',
], 200); public function publish_form($request)
{
$form_id = absint($request['id']);
// SNIP
// Promote draft โ published, clear draft. Design is untouched (top-level).
$json = $this->encode_slots($slots['content']['draft'], null, $slots['design'], $slots['settings']);
$saved = $this->save_post_content($form_id, $json);
if (! $saved) {
return new WP_REST_Response(['message' => 'Failed to publish form.'], 500);
}
return new WP_REST_Response([
'success' => true,
'form_id' => $form_id,
'message' => 'Form published successfully.',
], 200);id is usercontrolled input parameter in the url path
then it applies 3 checks to check the availablity of valid drafted form
then it encodes the form slots using the private function encode_slots and then it saves it using another private function save_post_content to update the wordpress database using wpdb->update()
and last it checks again is it saved or not.
Manual Exploitation
Now find a form which is drafted then attack on that drafted form to publish it without any authorization.
Enumerating Drafted Forms
the endpoint to find the drafted form is:
http://<target>/flowform/<id>/http://<target>/flowform/<id>/since finding manually is time consuming i also created an exploit for this which can easily find and attack on the form which i will demonstrate in next section.
in my testing lab the administrator created a form as a draft. the form id is 62
but it's not published we can check it as our low privilege user. with the url:
http://127.0.0.1/flowform/62/http://127.0.0.1/flowform/62/
it confirms that the form is in draft.
Exploitiation
now we can simply exploit since the vulnerable endpoint for rest API to publish this form is:
/flowforms/v1/forms/62/publish/flowforms/v1/forms/62/publishwe can simply send a post request since this endpoint and the form would be published.
The form is published.
Automated Exploit
I developed a complete exploit to enumerate drafted forms.
GitHub - 0x00phantom-hat/CVE-2026-11991-Exploit Contribute to 0x00phantom-hat/CVE-2026-11991-Exploit development by creating an account on GitHub.
The exploit enumerate the drafted forms and display it to user and prompt the user to choose from those available drafted forms.
attacker can adjust the range to find drafted forms and enables proxy for debugging or understanding the vulnerability
the form is published we can check it by id.
Patch Analysis
The developer implemented the patch. now the permission_callback doesn't use the current_user_can()
The developer created a function to check edit_post capability on the form id from the request. and implemented that function on the rest API Endpoints.
private function can_edit_form($form_id) {
return current_user_can('edit_post', $form_id);
}private function can_edit_form($form_id) {
return current_user_can('edit_post', $form_id);
}this function checks the capability of edit_post on the specific form id.
It make sure that the user is authorized to edit that form or not.
References
current_user_can() - Function | Developer.WordPress.org Returns whether the current user has the specified capability.
register_rest_route() - Function | Developer.WordPress.org Registers a REST API route.
CWE - CWE-639: Authorization Bypass Through User-Controlled Key (4.20) Common Weakness Enumeration (CWE) is a list of software weaknesses.