CVE-2026-49070

Knit Pay – Cashfree, Instamojo, Razorpay, PayPal and more <= 9.4.0.0 - Missing Authorization

mediumMissing Authorization
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
9.4.0.1
Patched in
8d
Time to patch

Description

The Knit Pay – Cashfree, Instamojo, Razorpay, PayPal and more plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in versions up to, and including, 9.4.0.0. This makes it possible for unauthenticated attackers to perform an unauthorized action.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=9.4.0.0
PublishedJune 8, 2026
Last updatedJune 15, 2026
Affected pluginknit-pay

What Changed in the Fix

Changes introduced in v9.4.0.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# CVE-2026-49070 Knit Pay – Missing Authorization Research Plan ## Vulnerability Summary The **Knit Pay** plugin (<= 9.4.0.0) is vulnerable to **Missing Authorization** in its PayPal gateway AJAX handlers. Specifically, the functions `ajax_create_order` and `ajax_capture_order` in `gateways/PayPal/…

Show full research plan

CVE-2026-49070 Knit Pay – Missing Authorization Research Plan

Vulnerability Summary

The Knit Pay plugin (<= 9.4.0.0) is vulnerable to Missing Authorization in its PayPal gateway AJAX handlers. Specifically, the functions ajax_create_order and ajax_capture_order in gateways/PayPal/Gateway.php perform security checks using WordPress nonces (check_ajax_referer) but fail to verify if the current user has the authority to modify the specific payment object identified by the payment_id parameter.

Because these handlers are likely registered as wp_ajax_nopriv_ actions (to support guest checkout), an unauthenticated attacker can obtain a valid nonce from any public payment page and then use it to manipulate the state or metadata (e.g., transaction_id) of any other pronamic_payment post in the system.

Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Actions:
    • knitpay_paypal_create_order (inferred from ajax_create_order)
    • knitpay_paypal_capture_order (inferred from ajax_capture_order)
  • Parameters:
    • action: The AJAX action name.
    • nonce: A valid nonce for the knitpay_paypal_checkout action.
    • payment_id: The integer ID of a pronamic_payment post.
    • order_id: (For capture) A PayPal order ID string.
  • Preconditions:
    • The PayPal gateway must be active.
    • An attacker needs to find a valid payment_id (enumerable via post IDs).
    • A valid nonce must be extracted from a page where the PayPal gateway is initialized.

Code Flow

  1. Entry Point: A POST request is sent to admin-ajax.php with the action knitpay_paypal_capture_order.
  2. Nonce Check: Gateway::ajax_capture_order() calls check_ajax_referer( 'knitpay_paypal_checkout', 'nonce', false ). This checks if the nonce is valid for the current user (or guest session).
  3. Object Retrieval: The code calls $payment = \get_pronamic_payment( $payment_id ).
  4. Vulnerable Sink: Without checking if the current user "owns" the payment session or has administrative privileges, the code proceeds to:
    • $payment->set_transaction_id( $order_id )
    • $gateway->update_status( $payment )
    • $payment->save()
  5. Impact: The transaction_id meta field of the payment post is updated in the database. If the provided order_id corresponds to a completed PayPal transaction (even from a different context), update_status may incorrectly mark the target payment as Success.

Nonce Acquisition Strategy

The nonce knitpay_paypal_checkout is generated for guest users (UID 0) and is valid for all guest sessions within the 12-24 hour window.

  1. Identify Script Localization: The plugin likely uses wp_localize_script to pass the nonce to the frontend PayPal Smart Buttons.
  2. Trigger Script Loading: The scripts are enqueued when a payment page for a PayPal-configured gateway is loaded.
  3. Extraction:
    • Create a payment link using the Knit Pay admin interface (or CLI).
    • Navigate to the payment link URL using browser_navigate.
    • Use browser_eval to extract the nonce:
      // Example target (based on common patterns in this plugin)
      window.knitpay_paypal_checkout_data?.nonce 
      // OR look for localized data in the page source
      
  4. Action Matching: In gateways/PayPal/Gateway.php, both handlers use the same action string: 'knitpay_paypal_checkout'.

Exploitation Strategy

  1. Setup:
    • Enable the PayPal gateway in Knit Pay.
    • Create a "target" payment (e.g., via a WooCommerce checkout or a Payment Link). Note its payment_id.
  2. Acquire Nonce:
    • Create a second "dummy" payment.
    • Visit the dummy payment page.
    • Extract the nonce for knitpay_paypal_checkout.
  3. Execute Attack:
    • Send a POST request to admin-ajax.php.
    • action=knitpay_paypal_capture_order
    • nonce=[EXTRACTED_NONCE]
    • payment_id=[TARGET_PAYMENT_ID]
    • order_id=MALICIOUS_TRANSACTION_123
  4. Verify: Check the metadata of the target payment post to confirm the transaction_id was changed.

Test Data Setup

  • Gateway: Create a pronamic_gateway post with post_title="PayPal" and configure it for the PayPal integration.
  • Payment: Create a pronamic_payment post representing a pending transaction.
  • Page: Create a WordPress page containing a payment button or link that uses the PayPal gateway to ensure the nonce is generated and localized.

Expected Results

  • The AJAX request returns a 200 OK (or 500 if the PayPal API call fails, but only after the transaction_id is set).
  • The target pronamic_payment post will have its _pronamic_payment_transaction_id (or similar meta key) updated to MALICIOUS_TRANSACTION_123.
  • An unauthenticated user successfully modifies data belonging to a transaction they did not initiate.

Verification Steps

Use WP-CLI to inspect the payment meta before and after the attack:

# Get current transaction ID
wp post meta get [PAYMENT_ID] _pronamic_payment_transaction_id

# After exploit, check again
wp post meta get [PAYMENT_ID] _pronamic_payment_transaction_id

Alternative Approaches

If ajax_capture_order fails due to strict PayPal API validation, target ajax_create_order:

  • Call knitpay_paypal_create_order with a target payment_id.
  • This triggers $gateway->api->create_order(...) and $payment->save().
  • Successful execution will overwrite the paypal_order_id meta and transaction_id of the target payment, which can be verified via WP-CLI.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Knit Pay plugin for WordPress is vulnerable to missing authorization in its PayPal gateway AJAX handlers. Unauthenticated attackers can manipulate the transaction ID and status of arbitrary payment records by leveraging a publicly accessible nonce to call sensitive functions without ownership verification.

Vulnerable Code

// gateways/PayPal/Gateway.php:93
public static function ajax_create_order() {
	if ( ! check_ajax_referer( 'knitpay_paypal_checkout', 'nonce', false ) ) {
		wp_send_json_error( [ 'message' => 'Security check failed. Please refresh the page and try again.' ], 403 );
	}

	$payment_id = isset( $_POST['payment_id'] ) ? absint( $_POST['payment_id'] ) : 0;
	// ...
	$payment = \get_pronamic_payment( $payment_id );
	// ...
			if ( isset( $payment_order->id ) ) {
				$payment->set_meta( 'paypal_order_id', $payment_order->id );
				$payment->set_transaction_id( $payment_order->id );
				$payment->save();


---

// gateways/PayPal/Gateway.php:153
public static function ajax_capture_order() {
	if ( ! check_ajax_referer( 'knitpay_paypal_checkout', 'nonce', false ) ) {
		wp_send_json_error( [ 'message' => 'Security check failed. Please refresh the page and try again.' ], 403 );
	}

	$order_id   = isset( $_POST['order_id'] ) ? sanitize_text_field( wp_unslash( $_POST['order_id'] ) ) : '';
	$payment_id = isset( $_POST['payment_id'] ) ? absint( $_POST['payment_id'] ) : 0;
	if ( empty( $order_id ) || empty( $payment_id ) ) {
		wp_send_json_error( [ 'message' => 'Invalid request parameters.' ], 400 );
	}

	$payment = \get_pronamic_payment( $payment_id );
	if ( null === $payment ) {
		wp_send_json_error( [ 'message' => 'Payment not found.' ], 404 );
	}

	try {
		$integration = new Integration();
		$gateway     = $integration->get_gateway( $payment->get_config_id() );

		$payment->set_transaction_id( $order_id );
		$gateway->update_status( $payment );
		$payment->save();

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/knit-pay/9.4.0.0/composer.lock /home/deploy/wp-safety.org/data/plugin-versions/knit-pay/9.4.0.1/composer.lock
--- /home/deploy/wp-safety.org/data/plugin-versions/knit-pay/9.4.0.0/composer.lock	2026-05-31 20:24:08.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/knit-pay/9.4.0.1/composer.lock	2026-06-04 08:44:12.000000000 +0000
@@ -3719,16 +3719,16 @@
         },
         {
             "name": "composer/composer",
-            "version": "2.10.0",
+            "version": "2.10.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/composer.git",
-                "reference": "c13824d95608b15913a7c0def0a3dea4474b71fc"
+                "reference": "4120703b9bda8795075047b40361d7ec4d2abe49"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/composer/zipball/c13824d95608b15913a7c0def0a3dea4474b71fc",
-                "reference": "c13824d95608b15913a7c0def0a3dea4474b71fc",
+                "url": "https://api.github.com/repos/composer/composer/zipball/4120703b9bda8795075047b40361d7ec4d2abe49",
+                "reference": "4120703b9bda8795075047b40361d7ec4d2abe49",
                 "shasum": ""
             },
             "require": {
@@ -3816,7 +3816,7 @@
                 "irc": "ircs://irc.libera.chat:6697/composer",
                 "issues": "https://github.com/composer/composer/issues",
                 "security": "https://github.com/composer/composer/security/policy",
-                "source": "https://github.com/composer/composer/tree/2.10.0"
+                "source": "https://github.com/composer/composer/tree/2.10.1"
             },
             "funding": [
                 {
@@ -3828,7 +3828,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2026-05-28T09:22:08+00:00"
+            "time": "2026-06-04T08:25:59+00:00"
         },
         {
             "name": "composer/metadata-minifier",
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/knit-pay/9.4.0.0/gateways/PayPal/Gateway.php /home/deploy/wp-safety.org/data/plugin-versions/knit-pay/9.4.0.1/gateways/PayPal/Gateway.php
--- /home/deploy/wp-safety.org/data/plugin-versions/knit-pay/9.4.0.0/gateways/PayPal/Gateway.php	2026-05-14 13:29:52.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/knit-pay/9.4.0.1/gateways/PayPal/Gateway.php	2026-06-04 08:44:12.000000000 +0000
@@ -153,9 +153,8 @@
 			wp_send_json_error( [ 'message' => 'Security check failed. Please refresh the page and try again.' ], 403 );
 		}
 
-		$order_id   = isset( $_POST['order_id'] ) ? sanitize_text_field( wp_unslash( $_POST['order_id'] ) ) : '';
 		$payment_id = isset( $_POST['payment_id'] ) ? absint( $_POST['payment_id'] ) : 0;
-		if ( empty( $order_id ) || empty( $payment_id ) ) {
+		if ( empty( $payment_id ) ) {
 			wp_send_json_error( [ 'message' => 'Invalid request parameters.' ], 400 );
 		}
 
@@ -164,11 +163,15 @@
 			wp_send_json_error( [ 'message' => 'Payment not found.' ], 404 );
 		}
 
+		$order_id = $payment->get_meta( 'paypal_order_id' );
+		if ( empty( $order_id ) ) {
+			wp_send_json_error( [ 'message' => 'No PayPal order found for this payment.' ], 400 );
+		}
+
 		try {
 			$integration = new Integration();
 			$gateway     = $integration->get_gateway( $payment->get_config_id() );
 
-			$payment->set_transaction_id( $order_id );
 			$gateway->update_status( $payment );
 			$payment->save();

Exploit Outline

An unauthenticated attacker can manipulate payment records by first obtaining a valid 'knitpay_paypal_checkout' AJAX nonce from any public-facing page where the PayPal gateway is initialized. With this nonce, the attacker can send a POST request to admin-ajax.php with the action 'knitpay_paypal_capture_order'. By providing a target 'payment_id' (which can be enumerated) and a controlled 'order_id' string, the attacker forces the plugin to update the transaction ID and re-verify the payment status for that record. Since the function lacks ownership checks, this allows an attacker to modify metadata and potentially trigger status updates for payments they did not initiate.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.