Published : August 7, 2026

CVE-2026-15054: Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder < 3.1.2 Missing Authorization to Unauthenticated Unauthorized Form Submission PoC, Patch Analysis & Rule

Plugin bit-form
Severity Medium (CVSS 5.3)
CWE 862
Vulnerable Version 3.1.2
Patched Version 3.1.2
Disclosed July 29, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-15054: Bit Form versions prior to 3.1.2 lack a status check on the frontend form submission and update handlers. This allows unauthenticated attackers to interact with and submit forms that have been deactivated or otherwise restricted, bypassing a security control. The vulnerability has a CVSS score of 5.3, indicating a moderate severity issue, and is classified under CWE-862 (Missing Authorization).

Root Cause: The root cause is a missing authorization check in several functions within the ‘FrontendAjax’ class, located in ‘bit-form/includes/Frontend/Ajax/FrontendAjax.php’. The vulnerable methods, including the AJAX handlers for form submission, entry updates, and form data retrieval, do not verify the active status of the form before performing operations. Specifically, the ‘formSubmit’ (around line 45), ‘submitForm’ (around line 63), ‘updateForm’ (around line 92), ‘getHiddenFields’ (around line 157), and ‘workFlowTrigger’ (around line 180) methods all fail to call a status-checking function like ‘checkStatus()’ on the ‘FrontendFormManager’ instance. This omission means that any form, even those marked inactive, remains fully operational through the frontend AJAX endpoints.

Exploitation: An unauthenticated attacker can exploit this by sending direct POST requests to the vulnerable AJAX endpoints. The primary endpoint is ‘/wp-admin/admin-ajax.php’. To submit a form, the attacker needs the form ID and a valid nonce or token. While the nonce is typically generated for legitimate users, the missing status check does not enforce this on the server side for the form’s active state. The attacker can obtain a valid nonce by loading the form’s public-facing page, which contains the necessary token. By crafting a request with the ‘action’ parameter set to ‘bitforms_submit_form’ (or the relevant action hook) and including the required form data and the obtained nonce, the attacker can submit entries to a deactivated form. Similarly, the ‘updateFormEntry’ function can be targeted by providing a valid entry token, allowing unauthorized modification of existing submissions.

Patch Analysis: The patch introduces calls to the ‘checkStatus()’ method on the ‘FrontendFormManager’ instance within all identified vulnerable methods in ‘FrontendAjax.php’. Before processing any request, the code now checks if the form exists and is active. If the form is not active, the handler returns a JSON error response with a 403 status code. This prevents any interaction with inactive forms. The patch also adds or modifies a ‘isExist()’ check in some functions to ensure the form is valid. This server-side enforcement closes the authorization gap by ensuring that form status is verified at the point of handling the request, effectively blocking for submission to deactivated forms regardless of the client-side state.

Impact: The primary impact is a violation of the form owner’s intended configuration. If a form is temporarily deactivated, for example, to stop collecting submissions, unauthorized submissions can still be created. This leads to the collection of unintended data, potential data integrity issues, and could result in the processing of information that should not have been collected. While this does not directly lead to other severe impacts like privilege escalation or remote code execution, it compromises the administrative control over the plugin’s resources and can have significant operational and compliance implications.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/bit-form/bitforms.php
+++ b/bit-form/bitforms.php
@@ -4,7 +4,7 @@
  * Plugin Name: Bit Form
  * Plugin URI:  https://www.bitapps.pro/bit-form
  * Description: Contact Form Builder Plugin: Multi Step Contact Form, Payment Form, Custom Contact Form Plugin by Bit Form
- * Version:     3.1.1
+ * Version:     3.1.2
  * Author:      Contact Form Builder - Bit Form
  * Author URI:  https://www.bitapps.pro
  * Text Domain: bit-form
@@ -22,7 +22,7 @@
 }

 // Define most essential constants.
-define('BITFORMS_VERSION', '3.1.1');
+define('BITFORMS_VERSION', '3.1.2');
 define('BITFORMS_PLUGIN_MAIN_FILE', __FILE__);
 define('BITFORMS_REQUIRED_BITFORMPRO_VERSION', '3.1.0');

--- a/bit-form/includes/Core/Form/FormManager.php
+++ b/bit-form/includes/Core/Form/FormManager.php
@@ -909,6 +909,30 @@
     return $updatedValue;
   }

+  private function normalizeOldFileValues($stored_files, $old_values)
+  {
+    $stored_files = is_array($stored_files) ? $stored_files : [];
+    $old_values = is_array($old_values) ? $old_values : explode(',', (string) $old_values);
+
+    $normalized_values = [];
+    foreach ($old_values as $value) {
+      if (!is_string($value) && !is_numeric($value)) {
+        continue;
+      }
+
+      $trimmed_value = trim((string) $value);
+      if ('' === $trimmed_value) {
+        continue;
+      }
+
+      if (in_array($trimmed_value, $stored_files, true)) {
+        $normalized_values[] = $trimmed_value;
+      }
+    }
+
+    return array_values(array_unique($normalized_values));
+  }
+
   public function updateFormEntry($updatedValue, $formID, $entryID)
   {
     // CSRF / entry-token verified upstream via FrontendFormManager::handleUpdateEntry() before this method is invoked.
@@ -1020,7 +1044,10 @@
                 if (isset($repeaterRow[$field_key]) && !empty($repeaterRow[$field_key])) {
                   $repeaterExistFiles[$index] = json_decode($repeaterRow[$field_key], true);
                 }
-                $repeaterFiles_old[$index] = empty($updatedValue[$field_key . '_old'][$index]) ? [] : explode(',', $updatedValue[$field_key . '_old'][$index]);
+                if (!is_array($repeaterExistFiles[$index])) {
+                  $repeaterExistFiles[$index] = [];
+                }
+                $repeaterFiles_old[$index] = $this->normalizeOldFileValues($repeaterExistFiles[$index], empty($updatedValue[$field_key . '_old'][$index]) ? [] : $updatedValue[$field_key . '_old'][$index]);
                 $repeaterDeleted_files[$index] = array_diff($repeaterExistFiles[$index], $repeaterFiles_old[$index]);
                 $fileHandler->deleteFiles($formID, $entryID, $repeaterDeleted_files[$index]);
               }
@@ -1035,8 +1062,11 @@
               ]
             );
             if (!is_wp_error($file_exists) && count($file_exists) > 0) {
-              $files_in_db = json_decode($file_exists[0]->meta_value);
-              $files_old = empty($updatedValue[$field_key . '_old']) ? [] : explode(',', $updatedValue[$field_key . '_old']);
+              $files_in_db = json_decode($file_exists[0]->meta_value, true);
+              if (!is_array($files_in_db)) {
+                $files_in_db = [];
+              }
+              $files_old = $this->normalizeOldFileValues($files_in_db, empty($updatedValue[$field_key . '_old']) ? [] : $updatedValue[$field_key . '_old']);
               $deleted_file = array_diff($files_in_db, $files_old);
               if (count($deleted_file) > 0) {
                 $fileHandler->deleteFiles($formID, $entryID, $deleted_file);
--- a/bit-form/includes/Core/Util/FileHandler.php
+++ b/bit-form/includes/Core/Util/FileHandler.php
@@ -122,11 +122,57 @@
     return $file_upoalded;
   }

+  public static function isSafeFileName($name)
+  {
+    if (!is_string($name)) {
+      return false;
+    }
+
+    $trimmed = trim($name);
+    if ('' === $trimmed) {
+      return false;
+    }
+
+    $baseName = basename($trimmed);
+    if ('' === $baseName || $baseName !== $trimmed || 'index.php' === $baseName) {
+      return false;
+    }
+
+    return sanitize_file_name($trimmed) === $trimmed;
+  }
+
   public function deleteFiles($form_id, $entry_id, $files)
   {
     $_upload_dir = self::getEntriesFileUploadDir($form_id, $entry_id);
-    foreach ($files as $name) {
-      wp_delete_file($_upload_dir . DIRECTORY_SEPARATOR . $name);
+    $resolvedBitformsUploadDir = realpath(BITFORMS_UPLOAD_DIR);
+    $resolvedUploadDir = realpath($_upload_dir);
+    if (false === $resolvedBitformsUploadDir || false === $resolvedUploadDir) {
+      return;
+    }
+
+    $bitformsUploadDirPrefix = trailingslashit(wp_normalize_path($resolvedBitformsUploadDir));
+    $uploadDirPrefix = trailingslashit(wp_normalize_path($resolvedUploadDir));
+    if (0 !== strpos($uploadDirPrefix, $bitformsUploadDirPrefix)) {
+      return;
+    }
+
+    foreach ((array) $files as $name) {
+      if (!self::isSafeFileName($name)) {
+        continue;
+      }
+
+      $candidatePath = $resolvedUploadDir . DIRECTORY_SEPARATOR . $name;
+      $resolvedPath = realpath($candidatePath);
+      if (false === $resolvedPath || !is_file($resolvedPath)) {
+        continue;
+      }
+
+      $normalizedPath = wp_normalize_path($resolvedPath);
+      if (0 !== strpos($normalizedPath, $uploadDirPrefix)) {
+        continue;
+      }
+
+      wp_delete_file($resolvedPath);
     }
   }

--- a/bit-form/includes/Frontend/Ajax/FrontendAjax.php
+++ b/bit-form/includes/Frontend/Ajax/FrontendAjax.php
@@ -45,6 +45,9 @@
       wp_send_json_error(__('Form ID not found', 'bit-form'), 400);
     }
     $FrontendFormManager = FrontendFormManager::getInstance($form_id);
+    if (!$FrontendFormManager->checkStatus()) {
+      wp_send_json_error(__('Form is not active', 'bit-form'), 403);
+    }
     $FrontendFormManager->fieldNameReplaceOfPost();
     $validateStatus = $FrontendFormManager->beforeSubmittedValidate(false);
     if (is_wp_error($validateStatus)) {
@@ -60,6 +63,9 @@
     // CSRF verified inside FrontendFormManager::handleSubmission() via verifySubmissionNonce() using HMAC-SHA256 token (Helpers::csrfDecrypted).
     $form_id = isset($_POST['bitforms_id']) ? str_replace('bitforms_', '', sanitize_text_field(wp_unslash($_POST['bitforms_id']))) : '';
     $FrontendFormManager = FrontendFormManager::getInstance($form_id);
+    if (!$FrontendFormManager->checkStatus()) {
+      wp_send_json_error(__('Form is not active', 'bit-form'), 403);
+    }
     $submitSatus = $FrontendFormManager->handleSubmission();
     if (is_wp_error($submitSatus)) {
       do_action('bitform_submit_error', $form_id, $submitSatus);
@@ -86,6 +92,9 @@
     $GLOBALS['bitform_entry_id'] = $entryId;
     if (Helpers::validateEntryTokenAndUser($entryToken, $entryId) || FrontendHelpers::is_current_user_can_access($form_id, 'entryEditAccess')) {
       $FrontendFormManager = FrontendFormManager::getInstance($form_id);
+      if (!$FrontendFormManager->checkStatus()) {
+        wp_send_json_error(__('Form is not active', 'bit-form'), 403);
+      }
       $updateStatus = $FrontendFormManager->handleUpdateEntry();
       if (is_wp_error($updateStatus)) {
         do_action('bitform_update_error', $form_id, $updateStatus);
@@ -148,6 +157,10 @@
         wp_send_json_error('Form Id not found', 400);
       } else {
         $formId = absint($data->formId);
+        $frontendFormManager = FrontendFormManager::getInstance($formId);
+        if (!$frontendFormManager->isExist() || !$frontendFormManager->checkStatus()) {
+          wp_send_json_error(__('Form is not active', 'bit-form'), 403);
+        }
         $fields = $this->hiddenFields($formId);
         $properties = $this->hiddenPropeties($formId);
         wp_send_json_success(['hidden_fields'=>$fields, 'hidden_properties'=>$properties]);
@@ -167,6 +180,11 @@
       $submitted_fields = [];
       if (isset($request->id, $request->cronNotOk)) {
         $formID = absint(str_replace('bitforms_', '', sanitize_text_field($request->id)));
+        $frontendFormManager = FrontendFormManager::getInstance($formID);
+        if (!$frontendFormManager->isExist() || !$frontendFormManager->checkStatus()) {
+          Log::debug_log('Inactive or non-existent form for workflow trigger. FormID=' . $formID);
+          wp_send_json_error(['message' => 'Form is not active'], 403);
+        }
         $cronNotOk = $request->cronNotOk;

         // Validate and sanitize entry ID and log ID
--- a/bit-form/vendor/composer/installed.php
+++ b/bit-form/vendor/composer/installed.php
@@ -1,9 +1,9 @@
 <?php return array(
     'root' => array(
         'name' => 'bitcode/bitform',
-        'pretty_version' => 'v3.1.1',
-        'version' => '3.1.1.0',
-        'reference' => 'd285d8ff15c6b2784bf33e0f335e63fe553385ad',
+        'pretty_version' => 'v3.1.2',
+        'version' => '3.1.2.0',
+        'reference' => '30f7ba5d513fa06bb86e85efe22fed3bdc4f7d65',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -20,9 +20,9 @@
             'dev_requirement' => false,
         ),
         'bitcode/bitform' => array(
-            'pretty_version' => 'v3.1.1',
-            'version' => '3.1.1.0',
-            'reference' => 'd285d8ff15c6b2784bf33e0f335e63fe553385ad',
+            'pretty_version' => 'v3.1.2',
+            'version' => '3.1.2.0',
+            'reference' => '30f7ba5d513fa06bb86e85efe22fed3bdc4f7d65',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.