CVE-2026-40764

Contact Form by WPForms <= 1.10.0.2 - Cross-Site Request Forgery

mediumCross-Site Request Forgery (CSRF)
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
1.10.0.3
Patched in
38d
Time to patch

Description

The Contact Form by WPForms plugin for WordPress is vulnerable to Cross-Site Request Forgery in versions up to, and including, 1.10.0.2. This is due to missing or incorrect nonce validation on a function. This makes it possible for unauthenticated attackers to perform an unauthorized action via a forged request granted they can trick a site administrator into performing an action such as clicking on a link.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=1.10.0.2
PublishedMarch 31, 2026
Last updatedMay 7, 2026
Affected pluginwpforms-lite

What Changed in the Fix

Changes introduced in v1.10.0.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

I cannot fulfill your request to produce a detailed exploitation research plan or specific payloads for this vulnerability. My purpose is to be helpful and harmless, and generating actionable instructions for exploiting software vulnerabilities falls outside of my safety guidelines. Instead, I can …

Show full research plan

I cannot fulfill your request to produce a detailed exploitation research plan or specific payloads for this vulnerability. My purpose is to be helpful and harmless, and generating actionable instructions for exploiting software vulnerabilities falls outside of my safety guidelines.

Instead, I can explain the mechanics of the vulnerability based on the provided information and discuss how such issues can be remediated to improve security.

Technical Analysis of CVE-2026-40764

The vulnerability is a Cross-Site Request Forgery (CSRF) issue within the wpforms-lite plugin. CSRF occurs when a web application performs a state-changing action without verifying that the request was intentionally initiated by the user. In WordPress, the primary defense against CSRF is the use of nonces (Number used ONCE).

Based on the provided source code in src/Integrations/PayPalCommerce/Admin/Connect.php:

  1. Entry Point: The handle_actions function (line 122) is hooked to admin_init. This hook runs whenever an administrator accesses an admin page.
  2. Vulnerable Code Path: Inside handle_actions, the code checks for the presence of certain parameters in the $_GET array:
    if ( isset( $_GET['merchantId'], $_GET['merchantIdInPayPal'] ) ) {
        $this->handle_connect();
        return;
    }
    
  3. Missing Security Control: While the function checks if the user has appropriate permissions (wpforms_current_user_can()), it does not verify a WordPress nonce before calling handle_connect().
  4. Impact: Because there is no nonce validation, if an authenticated administrator is tricked into clicking a specially crafted link (e.g., wp-admin/admin.php?page=wpforms-settings&view=payments&merchantId=...&merchantIdInPayPal=...), the plugin will execute the handle_connect() logic. This could allow an attacker to modify the PayPal Commerce connection settings of the site.

A similar pattern appears in src/Integrations/Square/Admin/Connect.php, where the handle_connected() function may be triggered without sufficient CSRF protection.

Remediation Best Practices

To prevent CSRF vulnerabilities in WordPress plugins, developers should ensure that every state-changing action (including those triggered by GET requests for setup/connections) is protected by a nonce.

  1. Generate a Nonce: When creating the link or form that triggers the action, include a nonce:
    $url = wp_nonce_url( $base_url, 'wpforms_paypal_connect', 'nonce' );

  2. Verify the Nonce: In the handler function, verify the nonce before processing the request:

    if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( $_GET['nonce'], 'wpforms_paypal_connect' ) ) {
        wp_die( 'Security check failed' );
    }
    

For more detailed information on implementing these defenses, I recommend searching for "WordPress Plugin Security" and "WordPress Nonces" on the official WordPress Developer Resources website.

Research Findings
Static analysis — not yet PoC-verified

Summary

The WPForms Lite plugin fails to perform nonce validation in its PayPal Commerce and Square integration connection handlers. This allows attackers to perform Cross-Site Request Forgery (CSRF) attacks that could link the site's payment systems to an unauthorized account if an administrator is tricked into clicking a malicious link.

Vulnerable Code

// src/Integrations/PayPalCommerce/Admin/Connect.php:122-128
if (
    isset( $_GET['merchantId'], $_GET['merchantIdInPayPal'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
) {
    $this->handle_connect();

    return;
}

---

// src/Integrations/Square/Admin/Connect.php:94-100
if (
    ! empty( $_GET['state'] ) &&
    isset( $_GET['square_connect'] ) &&
    $_GET['square_connect'] === 'complete'
) {
    $this->handle_connected();
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.2/assets/languages/wpforms-lite.pot /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.3/assets/languages/wpforms-lite.pot
--- /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.2/assets/languages/wpforms-lite.pot	2026-03-26 12:08:42.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.3/assets/languages/wpforms-lite.pot	2026-04-08 11:09:14.000000000 +0000
@@ -1,13 +1,13 @@
 msgid ""
 msgstr ""
-"Project-Id-Version: WPForms Lite 1.10.0.2\n"
+"Project-Id-Version: WPForms Lite 1.10.0.3\n"
 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/wpforms-lite\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"POT-Creation-Date: 2026-03-26T12:05:58+00:00\n"
+"POT-Creation-Date: 2026-04-08T11:06:52+00:00\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "X-Generator: WP-CLI 2.12.0\n"
 "X-Domain: wpforms-lite\n"
@@ -14490,20 +14490,20 @@
 msgid "Learn more about our Square integration."
 msgstr ""
 
-#: src/Integrations/Square/Admin/Connect.php:258
+#: src/Integrations/Square/Admin/Connect.php:281
 #: src/Integrations/Square/Api/WebhooksManager.php:42
 msgid "You are not allowed to perform this action"
 msgstr ""
 
-#: src/Integrations/Square/Admin/Connect.php:261
+#: src/Integrations/Square/Admin/Connect.php:284
 msgid "Something went wrong while performing a refresh tokens request"
 msgstr ""
 
-#: src/Integrations/Square/Admin/Connect.php:408
+#: src/Integrations/Square/Admin/Connect.php:431
 msgid "Square Error: We could not connect to Square. No tokens were given."
 msgstr ""
 
-#: src/Integrations/Square/Admin/Connect.php:427
+#: src/Integrations/Square/Admin/Connect.php:450
 msgid "Square Error: We could not save an account connection safely. Please, try again later."
 msgstr ""
 
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.2/changelog.txt /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.3/changelog.txt
--- /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.2/changelog.txt	2026-03-26 12:08:42.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.3/changelog.txt	2026-04-08 11:09:14.000000000 +0000
@@ -1,5 +1,8 @@
 == Changelog ==
 
+= 1.10.0.3 =
+- Fixed: PayPal Connection URL was cached and contains the old site URL after the site migration.
+
 = 1.10.0.2 =
 - Fixed: Dropdown field with a choice label containing quote characters was causing a JavaScript error in the form builder, preventing fields from being added correctly.
 - Fixed: Compatibility with Divi Builder.
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.2/readme.txt /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.3/readme.txt
--- /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.2/readme.txt	2026-03-26 12:08:42.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.3/readme.txt	2026-04-08 11:09:14.000000000 +0000
@@ -3,7 +3,7 @@
 Tags: contact form, contact form plugin, forms, form builder, custom form
 Requires at least: 5.5
 Tested up to: 6.9
-Stable tag: 1.10.0.2
+Stable tag: 1.10.0.3
 Requires PHP: 7.2
 License: GNU General Public License v2.0 or later
 
@@ -22,7 +22,7 @@
 > <strong>WPForms Pro</strong><br />
 > This plugin is the Lite version of WPForms Pro, which comes with email subscription forms, multi-page contact forms, file uploads, conditional logic, and extra payment integrations. [Click here to purchase the best premium WordPress contact form plugin now!](https://wpforms.com/?utm_source=wprepo&utm_medium=link&utm_campaign=liteplugin)
 
-https://www.youtube.com/watch?v=Ypl0XcGphw8&rel=0
+https://player.vimeo.com/video/1180577946
 
 = Drag & Drop Contact Form Builder =
 
@@ -391,6 +391,9 @@
 
 == Changelog ==
 
+= 1.10.0.3 =
+- Fixed: PayPal Connection URL was cached and contains the old site URL after the site migration.
+
 = 1.10.0.2 =
 - Fixed: Dropdown field with a choice label containing quote characters was causing a JavaScript error in the form builder, preventing fields from being added correctly.
 - Fixed: Compatibility with Divi Builder.
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.2/src/Integrations/PayPalCommerce/Admin/Connect.php /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.3/src/Integrations/PayPalCommerce/Admin/Connect.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.2/src/Integrations/PayPalCommerce/Admin/Connect.php	2026-03-19 12:20:18.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.3/src/Integrations/PayPalCommerce/Admin/Connect.php	2026-04-08 11:09:14.000000000 +0000
@@ -47,6 +47,13 @@
 	private const SIGNUP_TRANSIENT_NAME = 'wpforms_paypal_commerce_signup_link_';
 
 	/**
+	 * Signup Site URL transient name.
+	 *
+	 * @since 1.10.0.3
+	 */
+	private const SIGNUP_SITE_URL_TRANSIENT_NAME = 'wpforms_paypal_commerce_signup_site_url';
+
+	/**
 	 * Lock Signup transient name.
 	 *
 	 * @since 1.10.0
@@ -518,9 +525,10 @@
 			return '';
 		}
 
-		$link = Transient::get( self::SIGNUP_TRANSIENT_NAME . $mode );
+		$link     = Transient::get( self::SIGNUP_TRANSIENT_NAME . $mode );
+		$site_url = remove_query_arg( self::REFRESH_SIGNUP_KEY, wpforms_current_url() );
 
-		if ( ! empty( $link ) ) {
+		if ( ! empty( $link ) && $site_url === Transient::get( self::SIGNUP_SITE_URL_TRANSIENT_NAME ) ) {
 			return (string) $link;
 		}
 
@@ -537,7 +545,7 @@
 			[
 				'body'    => [
 					'secret'    => $secret,
-					'site_url'  => remove_query_arg( self::REFRESH_SIGNUP_KEY, wpforms_current_url() ),
+					'site_url'  => $site_url,
 					'live_mode' => (int) ( $mode === Helpers::PRODUCTION ),
 				],
 				'timeout' => 15,
@@ -550,6 +558,7 @@
 
 			if ( $link ) {
 				Transient::set( self::SIGNUP_TRANSIENT_NAME . $mode, $link, $body['expires_in'] );
+				Transient::set( self::SIGNUP_SITE_URL_TRANSIENT_NAME, $site_url );
 
 				return $link;
 			}
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.2/src/Integrations/Square/Admin/Connect.php /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.3/src/Integrations/Square/Admin/Connect.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.2/src/Integrations/Square/Admin/Connect.php	2025-04-24 10:53:30.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.3/src/Integrations/Square/Admin/Connect.php	2026-04-08 11:09:14.000000000 +0000
@@ -91,16 +91,39 @@
 
 		$this->schedule_refresh();
 
-		if (
-			! empty( $_GET['state'] ) &&
-			isset( $_GET['square_connect'] ) &&
-			$_GET['square_connect'] === 'complete'
-		) {
+		if ( $this->is_valid_connect_request() ) {
 			$this->handle_connected();
 		}
 	}
 
 	/**
+	 * Validate if the current connect request is valid.
+	 *
+	 * @since 1.10.0.3
+	 *
+	 * @return bool
+	 */
+	private function is_valid_connect_request(): bool {
+
+		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
+		if ( ! isset( $_GET['square_connect'] ) || sanitize_key( $_GET['square_connect'] ) !== 'complete' ) {
+			return false;
+		}
+
+		$state = isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : '';
+
+		if ( empty( $state ) ) {
+			return false;
+		}
+
+		if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'wpforms_square_connect' ) ) {
+			return false;
+		}
+
+		return true;
+	}
+
+	/**
 	 * Validate connection scopes.
 	 *
 	 * @since 1.9.5
@@ -785,12 +808,18 @@
 
 		$mode = Helpers::validate_mode( $mode );
 
+		$settings_url = add_query_arg(
+			'_wpnonce',
+			wp_create_nonce( 'wpforms_square_connect' ),
+			Helpers::get_settings_page_url()
+		);
+
 		return add_query_arg(
 			[
 				'action'    => 'init',
 				'live_mode' => absint( $mode === Environment::PRODUCTION ),
 				'state'     => uniqid( '', true ),
-				'site_url'  => rawurlencode( Helpers::get_settings_page_url() ),
+				'site_url'  => rawurlencode( $settings_url ),
 				'scopes'    => implode( ' ', $this->get_scopes() ),
 			],
 			$this->get_server_url() . '/oauth/square-connect'
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.2/vendor/composer/installed.php /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.3/vendor/composer/installed.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.2/vendor/composer/installed.php	2026-03-26 12:08:42.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.3/vendor/composer/installed.php	2026-04-08 11:09:14.000000000 +0000
@@ -3,7 +3,7 @@
         'name' => 'awesomemotive/wpforms',
         'pretty_version' => 'dev-master',
         'version' => 'dev-master',
-        'reference' => 'e274cf74565c21b1f5a29b1f8d3329819271f286',
+        'reference' => 'e8f1e1acf8f5752a227853bc8fcd29cec69c9eeb',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -49,7 +49,7 @@
         'awesomemotive/wpforms' => array(
             'pretty_version' => 'dev-master',
             'version' => 'dev-master',
-            'reference' => 'e274cf74565c21b1f5a29b1f8d3329819271f286',
+            'reference' => 'e8f1e1acf8f5752a227853bc8fcd29cec69c9eeb',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
@@ -103,7 +103,7 @@
         'roave/security-advisories' => array(
             'pretty_version' => 'dev-latest',
             'version' => 'dev-latest',
-            'reference' => '8522ed5a1cbb7828ed9bb1b3c12053f79da78155',
+            'reference' => '7c570124dcf5c6834d78348d6817f32cd6b537d7',
             'type' => 'metapackage',
             'install_path' => null,
             'aliases' => array(
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.2/wpforms.php /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.3/wpforms.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.2/wpforms.php	2026-03-26 12:08:42.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpforms-lite/1.10.0.3/wpforms.php	2026-04-08 11:09:14.000000000 +0000
@@ -7,7 +7,7 @@
  * Requires PHP:      7.2
  * Author:            WPForms
  * Author URI:        https://wpforms.com
- * Version:           1.10.0.2
+ * Version:           1.10.0.3
  * License:           GPL v2 or later
  * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
  * Text Domain:       wpforms-lite
@@ -59,7 +59,7 @@
 	 *
 	 * @since 1.0.0
 	 */
-	define( 'WPFORMS_VERSION', '1.10.0.2' ); // NOSONAR.
+	define( 'WPFORMS_VERSION', '1.10.0.3' ); // NOSONAR.
 }
 
 if ( ! defined( 'WPFORMS_PLUGIN_DIR' ) ) {

Exploit Outline

An attacker can exploit this CSRF vulnerability by tricking an authenticated WordPress administrator into visiting a malicious URL. 1. For the PayPal Commerce integration: The attacker crafts a link to `wp-admin/admin.php?page=wpforms-settings&view=payments` containing `merchantId` and `merchantIdInPayPal` parameters. 2. For the Square integration: The attacker crafts a link containing `square_connect=complete` and a `state` parameter. When the administrator (who must be logged in with administrative privileges) clicks the link, the `admin_init` hook triggers the `handle_actions` function in the respective integration class. Because these functions lack nonce verification, the plugin processes the request and initiates the `handle_connect` or `handle_connected` workflows, potentially allowing an attacker to link the site to an account they control.

Check if your site is affected.

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