REPORT / 01
Analysis Report · Folder Analysis cache/ninja-forms_3.13.2 → cache/ninja-forms_3.13.3 — CVE-2025-11924
Shared security patch analysis results
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-11924
NVD
AI-Generated Analysis
05 ·
Findings
filter · search · paginate
Showing 0 to 0 of 0 results
blocks/bootstrap.php
AI: 1 vulnerabilities
1 true positive
CVE-2025-11924
--- cache/ninja-forms_3.13.2/blocks/bootstrap.php 2026-01-22 13:51:21.459786420 +0000+++ cache/ninja-forms_3.13.3/blocks/bootstrap.php 2026-01-22 13:53:09.006444124 +0000@@ -273,51 +273,132 @@ * Generates a new token scoped to requested form IDs. * Used for automatic token refresh when tokens expire or after secret rotation. *- * Security: Public endpoint with rate limiting (10 requests per 5 minutes)+ * FIX: Restricts token generation to single forms and validates form access */ register_rest_route('ninja-forms-views', 'token/refresh', array( 'methods' => 'POST', 'callback' => function (WP_REST_Request $request) {- $formIds = $request->get_param('formIds');+ // REFACTOR: Accept single formID instead of formIds array+ $formId = $request->get_param('formID');+ + // Check for legacy formIds parameter for backward compatibility+ if (!$formId && $request->get_param('formIds')) {+ $formIds = $request->get_param('formIds');+ if (is_array($formIds) && !empty($formIds)) {+ // Only accept single form from legacy array+ if (count($formIds) > 1) {+ return new WP_Error(+ 'too_many_form_ids',+ __('Token generation is limited to one form at a time. Please use formID parameter instead.', 'ninja-forms'),+ array('status' => 400)+ );+ }+ $formId = $formIds[0];+ }+ } - // Validate form IDs- if (!is_array($formIds) || empty($formIds)) {+ // Sanitize and validate form ID+ $formId = absint($formId);+ + if (!$formId) { return new WP_Error(- 'invalid_form_ids',- __('Form IDs must be a non-empty array', 'ninja-forms'),+ 'invalid_form_id',+ __('Valid form ID is required', 'ninja-forms'), array('status' => 400) ); } - // Sanitize form IDs- $formIds = array_map('absint', $formIds);- $formIds = array_filter($formIds); // Remove zeros+ // FIX: Validate that the form exists and is accessible+ $form = Ninja_Forms()->form( $formId )->get();+ if (!$form) {+ return new WP_Error(+ 'form_not_found',+ __('The requested form does not exist', 'ninja-forms'),+ array('status' => 404)+ );+ } - if (empty($formIds)) {+ // FIX: Validate that user has permission to access this form+ // This prevents users from generating tokens for arbitrary forms+ $referer = wp_get_referer();+ if (!$referer) { return new WP_Error(- 'invalid_form_ids',- __('No valid form IDs provided', 'ninja-forms'),- array('status' => 400)+ 'invalid_request',+ __('Request must come from a valid page with submissions table block', 'ninja-forms'),+ array('status' => 403)+ );+ }++ // Parse the referring page to validate block authorization+ $post_id = url_to_postid($referer);+ if (!$post_id) {+ // Handle front page, archives, etc.+ $parsed_url = parse_url($referer);+ if ($parsed_url['path'] === '/' || $parsed_url['path'] === home_url('/')) {+ $post_id = get_option('page_on_front');+ }+ }++ // Check if the form is actually embedded in a submissions table block on this page+ if ($post_id) {+ $post = get_post($post_id);+ if ($post && has_blocks($post->post_content)) {+ $blocks = parse_blocks($post->post_content);+ $found_authorized_form = false;+ + // Recursively search for ninja-forms/submissions-table blocks+ $search_blocks = function($blocks) use ($formId, &$found_authorized_form, &$search_blocks) {+ foreach ($blocks as $block) {+ if ($block['blockName'] === 'ninja-forms/submissions-table') {+ if (isset($block['attrs']['formID']) && + intval($block['attrs']['formID']) === $formId) {+ $found_authorized_form = true;+ return;+ }+ }+ // Search inner blocks recursively+ if (!empty($block['innerBlocks'])) {+ $search_blocks($block['innerBlocks']);+ }+ }+ };+ + $search_blocks($blocks);+ + if (!$found_authorized_form) {+ return new WP_Error(+ 'unauthorized_form_access',+ __('You do not have permission to access this form via this page', 'ninja-forms'),+ array('status' => 403)+ );+ }+ }+ } else {+ // If we can't determine the post ID, return an error+ return new WP_Error(+ 'post_id_not_found',+ __('The requested data could not be related to a valid page', 'ninja-forms'),+ array('status' => 403) ); } - // Generate new token scoped to requested forms+ // Generate new token scoped to the single requested form $publicKey = NinjaForms\Blocks\Authentication\KeyFactory::make(32); $tokenGenerator = NinjaForms\Blocks\Authentication\TokenFactory::make();- $newToken = $tokenGenerator->create($publicKey, $formIds);+ $newToken = $tokenGenerator->create($publicKey, array($formId)); return array( 'token' => $newToken, 'publicKey' => $publicKey, 'expiresIn' => 900, // 15 minutes in seconds- 'formIds' => $formIds,+ 'formID' => $formId, // Changed from formIds to formID ); }, 'permission_callback' => function (WP_REST_Request $request) { // Apply stricter rate limiting to refresh endpoint $rateLimitCheck = NinjaForms\Blocks\Authentication\RateLimiter::check( '/ninja-forms-views/token/refresh',- 10, // limit: 10 requests+ 50, // limit: 50 requests 300 // window: 5 minutes ); @@ -325,7 +406,7 @@ return $rateLimitCheck; // Returns 429 Too Many Requests } - return true; // Public endpoint (rate-limited)+ return true; // Public endpoint (rate-limited) but with form validation }, ));
AI Analysis
Vulnerability Existed: yes
TRUE POSITIVE
[Broken Access Control / Horizontal Privilege Escalation] - [CWE-639] - blocks/bootstrap.php [273-404]
Old Code:
```php
$formIds = $request->get_param('formIds');
// Validate form IDs
if (!is_array($formIds) || empty($formIds)) {
return new WP_Error(
'invalid_form_ids',
__('Form IDs must be a non-empty array', 'ninja-forms'),
array('status' => 400)
);
}
// Sanitize form IDs
$formIds = array_map('absint', $formIds);
$formIds = array_filter($formIds); // Remove zeros
if (empty($formIds)) {
return new WP_Error(
'invalid_form_ids',
__('No valid form IDs provided', 'ninja-forms'),
array('status' => 400)
);
}
// Generate new token scoped to requested forms
$publicKey = NinjaForms\Blocks\Authentication\KeyFactory::make(32);
$tokenGenerator = NinjaForms\Blocks\Authentication\TokenFactory::make();
$newToken = $tokenGenerator->create($publicKey, $formIds);
return array(
'token' => $newToken,
'publicKey' => $publicKey,
'expiresIn' => 900,
'formIds' => $formIds,
);
```
Fixed Code:
```php
// REFACTOR: Accept single formID instead of formIds array
$formId = $request->get_param('formID');
// Check for legacy formIds parameter for backward compatibility
if (!$formId && $request->get_param('formIds')) {
$formIds = $request->get_param('formIds');
if (is_array($formIds) && !empty($formIds)) {
// Only accept single form from legacy array
if (count($formIds) > 1) {
return new WP_Error(
'too_many_form_ids',
__('Token generation is limited to one form at a time. Please use formID parameter instead.', 'ninja-forms'),
array('status' => 400)
);
}
$formId = $formIds[0];
}
}
// Sanitize and validate form ID
$formId = absint($formId);
if (!$formId) {
return new WP_Error(
'invalid_form_id',
__('Valid form ID is required', 'ninja-forms'),
array('status' => 400)
);
}
// FIX: Validate that the form exists and is accessible
$form = Ninja_Forms()->form( $formId )->get();
if (!$form) {
return new WP_Error(
'form_not_found',
__('The requested form does not exist', 'ninja-forms'),
array('status' => 404)
);
}
// FIX: Validate that user has permission to access this form
// This prevents users from generating tokens for arbitrary forms
$referer = wp_get_referer();
if (!$referer) {
return new WP_Error(
'invalid_request',
__('Request must come from a valid page with submissions table block', 'ninja-forms'),
array('status' => 403)
);
}
// Parse the referring page to validate block authorization
$post_id = url_to_postid($referer);
if (!$post_id) {
// Handle front page, archives, etc.
$parsed_url = parse_url($referer);
if ($parsed_url['path'] === '/' || $parsed_url['path'] === home_url('/')) {
$post_id = get_option('page_on_front');
}
}
// Check if the form is actually embedded in a submissions table block on this page
if ($post_id) {
$post = get_post($post_id);
if ($post && has_blocks($post->post_content)) {
$blocks = parse_blocks($post->post_content);
$found_authorized_form = false;
// Recursively search for ninja-forms/submissions-table blocks
$search_blocks = function($blocks) use ($formId, &$found_authorized_form, &$search_blocks) {
foreach ($blocks as $block) {
if ($block['blockName'] === 'ninja-forms/submissions-table') {
if (isset($block['attrs']['formID']) &&
intval($block['attrs']['formID']) === $formId) {
$found_authorized_form = true;
return;
}
}
// Search inner blocks recursively
if (!empty($block['innerBlocks'])) {
$search_blocks($block['innerBlocks']);
}
}
};
$search_blocks($blocks);
if (!$found_authorized_form) {
return new WP_Error(
'unauthorized_form_access',
__('You do not have permission to access this form via this page', 'ninja-forms'),
array('status' => 403)
);
}
}
} else {
// If we can't determine the post ID, return an error
return new WP_Error(
'post_id_not_found',
__('The requested data could not be related to a valid page', 'ninja-forms'),
array('status' => 403)
);
}
// Generate new token scoped to the single requested form
$publicKey = NinjaForms\Blocks\Authentication\KeyFactory::make(32);
$tokenGenerator = NinjaForms\Blocks\Authentication\TokenFactory::make();
$newToken = $tokenGenerator->create($publicKey, array($formId));
return array(
'token' => $newToken,
'publicKey' => $publicKey,
'expiresIn' => 900,
'formID' => $formId,
);
```
Explanation:
The old code accepted multiple form IDs in an array and generated tokens scoped to all requested forms without validating that the user had authorization to access those forms. An unauthenticated user could request token refresh for any form ID(s) by simply calling the REST endpoint with an arbitrary array of form IDs. This is a horizontal privilege escalation vulnerability (CWE-639: Authorization Bypass Through User-Controlled Key).
The fix restricts token generation to a single form at a time and implements proper access control validation by:
1. Changing from `formIds` array to single `formID` parameter
2. Verifying the form exists before generating tokens
3. Requiring valid HTTP referrer to prevent CSRF and direct API abuse
4. Validating that the requesting user's referrer page actually contains a submissions-table block for that specific form
5. Only generating tokens for forms the user is legitimately accessing through the WordPress block interface
This prevents users from generating valid tokens for forms they shouldn't have access to.
CVE Analysis Results:
CVE-2025-11924: Yes
View CVE Description
The Ninja Forms – The Contact Form Builder That Grows With You plugin for WordPress is vulnerable to Insecure Direct Object Reference in versions up to, and including, 3.13.2. This is due to the plugin not properly verifying that a user is authorized before the `ninja-forms-views` REST endpoints return form metadata and submission content. This makes it possible for unauthenticated attackers to read arbitrary form definitions and submission records via a leaked bearer token granted they can load any page containing the Submissions Table block. NOTE: The developer released a patch for this issue in 3.13.1, but inadvertently introduced a REST API endpoint in which a valid bearer token could be minted for arbitrary form IDs, making this patch ineffective.
Showing 1 to 1 of 1 results