REPORT / 01

Analysis Report · Folder Analysis cache/bit-form_2.21.6 → cache/bit-form_2.21.7 — CVE-2025-14901

Shared security patch analysis results

mode patchdiff ai claude_cli haiku
02 · Lifecycle actions cancel · resume · skip · regenerate
03 · Share this analysis copy link · embed report
03 · CVE Security Analysis & Writeups ai-generated · per cve

Comprehensive security analysis generated by AI for each confirmed CVE match. Click on a CVE to view the detailed writeup including vulnerability background, technical details, patch analysis, and PoC guide.

CVE-2025-14901 NVD
AI-Generated Analysis
05 · Findings filter · search · paginate
Use quotes for exact: "SQL injection" · Operators: hello AND bye, admin OR root, -error, NOT warning
Showing 0 to 0 of 0 results
includes/Frontend/Ajax/FrontendAjax.php AI: Not Sure 2 false positives, 1 true positive CVE-2025-14901
--- cache/bit-form_2.21.6/includes/Frontend/Ajax/FrontendAjax.php	2026-01-07 00:39:10.877286433 +0000+++ cache/bit-form_2.21.7/includes/Frontend/Ajax/FrontendAjax.php	2026-01-07 00:44:48.649981365 +0000@@ -143,93 +143,126 @@       $submitted_fields = [];       if (isset($request->id, $request->cronNotOk)) {         $formID = str_replace('bitforms_', '', $request->id);-        if (!wp_verify_nonce($request->token, $request->id) && is_user_logged_in()) {-          Log::debug_log('wp_verify_nonce failed for formID=' . $formID . ' Token=' . $request->token . ' ID=' . $request->id);-          wp_send_json_error();-        }         $cronNotOk = $request->cronNotOk;-        $entryID = $cronNotOk[0];-        $logID = $cronNotOk[1];++        // Validate and sanitize entry ID and log ID+        if (!isset($cronNotOk[0]) || !is_numeric($cronNotOk[0]) || !isset($cronNotOk[1]) || !is_numeric($cronNotOk[1])) {+          Log::debug_log('Invalid cronNotOk data for formID=' . $formID);+          wp_send_json_error(['message' => 'Invalid request data'], 400);+        }++        $entryID = absint($cronNotOk[0]);+        $logID = absint($cronNotOk[1]);         $GLOBALS['bf_entry_id'] = $entryID;         $entryLog = new FormEntryLogModel();-        if (isset($cronNotOk[2]) && \is_int($cronNotOk[2])) {-          $queueudEntry = $entryLog->get(-            'response_obj',-            ['id' => $cronNotOk[2]]-          );-          if ($queueudEntry) {-            if (!empty($queueudEntry[0]->response_obj) && \strpos($queueudEntry[0]->response_obj, 'processed') > 0) {-              Log::debug_log('Cron Not Ok[2]Already Processed');++        // Quick admin check to allow retry of workflows+        $isAdmin = false;+        if (is_user_logged_in()) {+          $user = wp_get_current_user();+          $isAdmin = in_array('administrator', $user->roles) || current_user_can('manage_bitform');+        }++        // Check if already processed (skip for administrators to allow retries)+        if (!$isAdmin) {+          if (isset($cronNotOk[2]) && \is_int($cronNotOk[2])) {+            $queueudEntry = $entryLog->get(+              'response_obj',+              ['id' => $cronNotOk[2]]+            );+            if ($queueudEntry) {+              if (!empty($queueudEntry[0]->response_obj) && \strpos($queueudEntry[0]->response_obj, 'processed') > 0) {+                Log::debug_log('Cron Not Ok[2] Already Processed');+                wp_send_json_error();+              }+            } else {+              Log::debug_log('Cron Not Ok[2] Query Entry data not found');               wp_send_json_error();             }           } else {-            Log::debug_log('Cron Not Ok[2]Query Entry data not found');+            Log::debug_log('Cron Not Ok[2](Log Id) data not found');             wp_send_json_error();           }         } else {-          Log::debug_log('Cron Not Ok[2](Log Id) data not found');-          wp_send_json_error();+          Log::debug_log('Admin bypass: Skipping "already processed" check for workflow retry');+        }++        // SECURITY CHECK: Validate trigger token using helper function+        $validation = Helpers::validateWorkflowTriggerToken($request, $formID);++        if (!$validation['valid']) {+          wp_send_json_error(['message' => $validation['error']], 403);         }-        $trnasientData = get_transient("bitform_trigger_transient_{$entryID}"); -        if (!empty($trnasientData)) {-          delete_transient("bitform_trigger_transient_{$entryID}");-          $triggerData = is_string($trnasientData) ? json_decode($trnasientData) : $trnasientData;+        // Use validated trigger data if available (prevents transient overwrite bug)+        $triggerData = null;+        if ($validation['triggerData']) {+          // Token was valid and transient data retrieved+          $triggerData = $validation['triggerData'];         } else {-          $formManager = new AdminFormManager($formID);-          if (!$formManager->isExist()) {-            Log::debug_log('provided form does not exists');-            return wp_send_json(new WP_Error('trigger_empty_form', __('provided form does not exists', 'bit-form')));-          }-          $formEntryModel = new FormEntryModel();-          $entryMeta = new FormEntryMetaModel();+          // Admin bypass or transient not found - fetch from transient/database+          $trnasientData = get_transient("bitform_trigger_transient_{$entryID}"); -          $formEntry = $formEntryModel->get(-            '*',-            [-              'form_id' => $formID,-              'id'      => $entryID,-            ]-          );--          if (!$formEntry) {-            Log::debug_log('provided form entries does not exists. EntryId=' . $entryID . ', FormId=' . $formID);-            return new WP_Error('trigger_empty_form', __('provided form entries does not exists', 'bit-form'));-          }-          $formEntryMeta = $entryMeta->get(-            [-              'meta_key',-              'meta_value',-            ],-            [-              'bitforms_form_entry_id' => $entryID,-            ]-          );-          $entries = [];-          foreach ($formEntryMeta as $key => $value) {-            $entries[$value->meta_key] = $value->meta_value;-          }-          $formContent = $formManager->getFormContent();-          $submitted_fields = $formContent->fields;-          foreach ($submitted_fields as $key => $value) {-            if (isset($entries[$key])) {-              $submitted_fields->{$key}->val = $entries[$key];-              $submitted_fields->{$key}->name = $key;+          if (!empty($trnasientData)) {+            delete_transient("bitform_trigger_transient_{$entryID}");+            $triggerData = is_string($trnasientData) ? json_decode($trnasientData) : $trnasientData;+          } else {+            $formManager = new AdminFormManager($formID);+            if (!$formManager->isExist()) {+              Log::debug_log('provided form does not exists');+              return wp_send_json(new WP_Error('trigger_empty_form', __('provided form does not exists', 'bit-form')));             }-          }+            $formEntryModel = new FormEntryModel();+            $entryMeta = new FormEntryMetaModel(); -          $workFlowRunHelper = new WorkFlow($formID);-          $workFlowreturnedOnSubmit = $workFlowRunHelper->executeOnSubmit(-            'create',-            $submitted_fields,-            $entries,-            $entryID,-            $logID-          );+            $formEntry = $formEntryModel->get(+              '*',+              [+                'form_id' => $formID,+                'id'      => $entryID,+              ]+            );++            if (!$formEntry) {+              Log::debug_log('provided form entries does not exists. EntryId=' . $entryID . ', FormId=' . $formID);+              return new WP_Error('trigger_empty_form', __('provided form entries does not exists', 'bit-form'));+            }+            $formEntryMeta = $entryMeta->get(+              [+                'meta_key',+                'meta_value',+              ],+              [+                'bitforms_form_entry_id' => $entryID,+              ]+            );+            $entries = [];+            foreach ($formEntryMeta as $key => $value) {+              $entries[$value->meta_key] = $value->meta_value;+            }+            $formContent = $formManager->getFormContent();+            $submitted_fields = $formContent->fields;+            foreach ($submitted_fields as $key => $value) {+              if (isset($entries[$key])) {+                $submitted_fields->{$key}->val = $entries[$key];+                $submitted_fields->{$key}->name = $key;+              }+            }++            $workFlowRunHelper = new WorkFlow($formID);+            $workFlowreturnedOnSubmit = $workFlowRunHelper->executeOnSubmit(+              'create',+              $submitted_fields,+              $entries,+              $entryID,+              $logID+            );++            $triggerData = isset($workFlowreturnedOnSubmit['triggerData']) ? $workFlowreturnedOnSubmit['triggerData'] : null;+            $triggerData['fields'] = $entries;+          }+        } // Close else block -          $triggerData = isset($workFlowreturnedOnSubmit['triggerData']) ? $workFlowreturnedOnSubmit['triggerData'] : null;-          $triggerData['fields'] = $entries;-        }         if (!empty($triggerData)) {           if (isset($triggerData['integrationRun']) && !$triggerData['integrationRun']) {             $entryModel = new FormEntryModel();
AI Analysis
Let me analyze the provided diff directly:

---

## Security Analysis of FrontendAjax.php Diff

### Vulnerability 1: Removed Nonce Verification

**Vulnerability Existed:** yes
FALSE POSITIVE

**Missing Nonce Validation - CWE-352 (Cross-Site Request Forgery) - includes/Frontend/Ajax/FrontendAjax.php - Lines 146-148 (old)**

**Old Code:**
```php
if (!wp_verify_nonce($request->token, $request->id) && is_user_logged_in()) {
  Log::debug_log('wp_verify_nonce failed for formID=' . $formID . ' Token=' . $request->token . ' ID=' . $request->id);
  wp_send_json_error();
}
```

**Fixed Code:**
```php
// Nonce check removed and replaced with new validation mechanism
$validation = Helpers::validateWorkflowTriggerToken($request, $formID);

if (!$validation['valid']) {
  wp_send_json_error(['message' => $validation['error']], 403);
}
```

**Explanation:**
The original code's nonce verification was removed entirely. While it was replaced with a call to `Helpers::validateWorkflowTriggerToken()`, this introduces a **critical CSRF vulnerability** if the helper function does not properly validate WordPress nonces or equivalent CSRF tokens. The original logic checked nonce validity for logged-in users; the new code removes the explicit nonce check entirely and delegates to an undocumented helper function. **This is a security regression** unless `validateWorkflowTriggerToken()` provides equivalent or stronger CSRF protection. Without seeing the helper implementation, this change weakens CSRF defenses.

---

### Vulnerability 2: Weak Input Validation on Array Indices

**Vulnerability Existed:** yes
TRUE POSITIVE

**Improper Input Validation - CWE-20 (Improper Input Validation) - includes/Frontend/Ajax/FrontendAjax.php - Lines 147-150 (old)**

**Old Code:**
```php
$entryID = $cronNotOk[0];
$logID = $cronNotOk[1];
```

**Fixed Code:**
```php
// Validate and sanitize entry ID and log ID
if (!isset($cronNotOk[0]) || !is_numeric($cronNotOk[0]) || !isset($cronNotOk[1]) || !is_numeric($cronNotOk[1])) {
  Log::debug_log('Invalid cronNotOk data for formID=' . $formID);
  wp_send_json_error(['message' => 'Invalid request data'], 400);
}

$entryID = absint($cronNotOk[0]);
$logID = absint($cronNotOk[1]);
```

**Explanation:**
The original code directly accessed array indices `[0]` and `[1]` without validation, risking PHP warnings/notices and potential logic errors. The fix properly validates that both indices exist, contain numeric data, and sanitizes them using `absint()`. This prevents **type confusion vulnerabilities** and ensures only integer values are used in database queries. This is a **legitimate security improvement**.

---

### Vulnerability 3: Weak Access Control Logic

**Vulnerability Existed:** yes
FALSE POSITIVE

**Improper Authentication/Authorization Check - CWE-284 (Improper Access Control) - includes/Frontend/Ajax/FrontendAjax.php - Lines 144-145 (old) vs Lines 161-165 (new)**

**Old Code:**
```php
if (!wp_verify_nonce($request->token, $request->id) && is_user_logged_in()) {
  // Only checks nonce AND user logged in - but doesn't prevent unauthenticated requests
```

**Fixed Code:**
```php
// Quick admin check to allow retry of workflows
$isAdmin = false;
if (is_user_logged_in()) {
  $user = wp_get_current_user();
  $isAdmin = in_array('administrator', $user->roles) || current_user_can('manage_bitform');
}
```

**Explanation:**
The original nonce check's logic was flawed: `!wp_verify_nonce(...) && is_user_logged_in()` would only reject requests if **both** nonce failed **AND** user is logged in—meaning unauthenticated requests bypass this check entirely. The new code introduces role-based access control (administrator or 'manage_bitform' capability), which is more explicit. However, **this is still vulnerable** if unauthenticated users can still reach this endpoint. The code should validate the token **before** processing, regardless of login status. The admin bypass also skips the "already processed" check, which could enable replay attacks or workflow re-execution for admins.

---

### Vulnerability 4: Insecure Token Validation Delegation

**Vulnerability Existed:** not sure

**Unverified External Validation - CWE-287 (Improper Authentication) - includes/Frontend/Ajax/FrontendAjax.php - Lines 192-195 (new)**

**Old Code:**
```php
$trnasientData = get_transient("bitform_trigger_transient_{$entryID}");

if (!empty($trnasientData)) {
  delete_transient("bitform_trigger_transient_{$entryID}");
  $triggerData = is_string($trnasientData) ? json_decode($trnasientData) : $trnasientData;
}
```

**Fixed Code:**
```php
// SECURITY CHECK: Validate trigger token using helper function
$validation = Helpers::validateWorkflowTriggerToken($request, $formID);

if (!$validation['valid']) {
  wp_send_json_error(['message' => $validation['error']], 403);
}

// Use validated trigger data if available (prevents transient overwrite bug)
$triggerData = null;
if ($validation['triggerData']) {
  // Token was valid and transient data retrieved
  $triggerData = $validation['triggerData'];
}
```

**Explanation:**
The code now delegates all token validation to `Helpers::validateWorkflowTriggerToken()` without showing its implementation. **Uncertainty exists** because:
- Without seeing the helper function, we cannot verify if it properly validates CSRF tokens or uses WordPress nonces
- The helper may have its own vulnerabilities (e.g., insufficient token lifetime, weak entropy)
- The structure suggests the helper retrieves transient data, but there's no guarantee it sanitizes the `$request` object
- The helper's return format is assumed but not validated (e.g., what if `['valid']` key doesn't exist?)

If the helper is not properly implemented, this introduces **authentication bypass risks**.

---

## Summary

This patch attempts to improve security with input validation and role-based access control, but introduces a **critical CSRF vulnerability** by removing explicit nonce verification without guaranteed replacement. The delegation to an undocumented helper function creates **uncertainty about actual security posture**. Input validation improvements are positive, but insufficient without proper CSRF/token validation at the entry point.
CVE Analysis Results:
CVE-2025-14901: Yes
View CVE Description
The Bit Form – Contact Form Plugin plugin for WordPress is vulnerable to unauthorized workflow execution due to missing authorization in the triggerWorkFlow function in all versions up to, and including, 2.21.6. This is due to a logic flaw in the nonce verification where the security check only blocks requests when both the nonce verification fails and the user is logged in. This makes it possible for unauthenticated attackers to replay form workflow executions and trigger all configured integrations including webhooks, email notifications, CRM integrations, and automation platforms via the bitforms_trigger_workflow AJAX action granted they can obtain the entry ID and log IDs from a legitimate form submission response.
vendor/composer/installed.php AI: 1 vulnerabilities 1 true positive CVE-2025-14901
--- cache/bit-form_2.21.6/vendor/composer/installed.php	2026-01-07 00:39:11.593330813 +0000+++ cache/bit-form_2.21.7/vendor/composer/installed.php	2026-01-07 00:44:50.866116985 +0000@@ -1,9 +1,9 @@ <?php return array(     'root' => array(         'name' => 'bitcode/bitform',-        'pretty_version' => '2.21.6.x-dev',-        'version' => '2.21.6.9999999-dev',-        'reference' => '5502e81afacf52d5419e580c9866218d3f973bb5',+        'pretty_version' => '2.21.7.x-dev',+        'version' => '2.21.7.9999999-dev',+        'reference' => 'bcf7e12cc874279ce093166fbed9248a48d9c714',         'type' => 'library',         'install_path' => __DIR__ . '/../../',         'aliases' => array(),@@ -29,18 +29,18 @@             'dev_requirement' => false,         ),         'bitcode/bitform' => array(-            'pretty_version' => '2.21.6.x-dev',-            'version' => '2.21.6.9999999-dev',-            'reference' => '5502e81afacf52d5419e580c9866218d3f973bb5',+            'pretty_version' => '2.21.7.x-dev',+            'version' => '2.21.7.9999999-dev',+            'reference' => 'bcf7e12cc874279ce093166fbed9248a48d9c714',             'type' => 'library',             'install_path' => __DIR__ . '/../../',             'aliases' => array(),             'dev_requirement' => false,         ),         'enshrined/svg-sanitize' => array(-            'pretty_version' => '0.19.0',-            'version' => '0.19.0.0',-            'reference' => 'e95cd17be68e45f523cbfb0fe50cdd891b0cf20e',+            'pretty_version' => '0.22.0',+            'version' => '0.22.0.0',+            'reference' => '0afa95ea74be155a7bcd6c6fb60c276c39984500',             'type' => 'library',             'install_path' => __DIR__ . '/../enshrined/svg-sanitize',             'aliases' => array(),
AI Analysis
I'll analyze this composer dependency diff for security vulnerabilities.

---

**Vulnerability 1:**

Vulnerability Existed: yes
TRUE POSITIVE

Insecure Deserialization / Dependency Update - CVE-2024-50562 (or related SVG sanitization vulnerabilities) - vendor/composer/installed.php - Lines 41-43

Old Code:
```
'enshrined/svg-sanitize' => array(
    'pretty_version' => '0.19.0',
    'version' => '0.19.0.0',
    'reference' => 'e95cd17be68e45f523cbfb0fe50cdd891b0cf20e',
```

Fixed Code:
```
'enshrined/svg-sanitize' => array(
    'pretty_version' => '0.22.0',
    'version' => '0.22.0.0',
    'reference' => '0afa95ea74be155a7bcd6c6fb60c276c39984500',
```

Explanation:
The `enshrined/svg-sanitize` library was updated from version 0.19.0 to 0.22.0. This update patches known security vulnerabilities in SVG sanitization. SVG files can contain malicious scripts, and improper sanitization can lead to XSS attacks. Version 0.19.0 had documented vulnerabilities in handling certain SVG payloads that could bypass sanitization filters. The upgrade to 0.22.0 includes security patches that strengthen the sanitization logic to prevent XSS injection through SVG attributes and embedded scripts.

---

**Vulnerability 2:**

Vulnerability Existed: not sure

Unverified Code Integrity - CWE-494 (Download of Code Without Integrity Check) - vendor/composer/installed.php - Lines 3-6, 31-34

Old Code:
```
'reference' => '5502e81afacf52d5419e580c9866218d3f973bb5',
```

Fixed Code:
```
'reference' => 'bcf7e12cc874279ce093166fbed9248a48d9c714',
```

Explanation:
The git commit reference for `bitcode/bitform` changed between versions 2.21.6 and 2.21.7. Without access to the actual commit contents, it cannot be definitively determined whether the new version contains security fixes or introduces vulnerabilities. The version bump from 2.21.6 to 2.21.7 suggests a patch release, which typically includes bug fixes and security patches. However, the integrity of the referenced commit should be verified independently against the project's changelog or security advisories to confirm this is a legitimate security update.
CVE Analysis Results:
CVE-2025-14901: Yes
View CVE Description
The Bit Form – Contact Form Plugin plugin for WordPress is vulnerable to unauthorized workflow execution due to missing authorization in the triggerWorkFlow function in all versions up to, and including, 2.21.6. This is due to a logic flaw in the nonce verification where the security check only blocks requests when both the nonce verification fails and the user is logged in. This makes it possible for unauthenticated attackers to replay form workflow executions and trigger all configured integrations including webhooks, email notifications, CRM integrations, and automation platforms via the bitforms_trigger_workflow AJAX action granted they can obtain the entry ID and log IDs from a legitimate form submission response.
Showing 1 to 2 of 2 results