CVE-2026-12165

Contest Gallery <= 30.0.2 - Authenticated (Author+) Privilege Escalation via 'RegistryUserRole' Parameter

highImproper Privilege Management
8.8
CVSS Score
8.8
CVSS Score
high
Severity
30.0.3
Patched in
1d
Time to patch

Description

The Contest Gallery – Upload & Vote Photos, Media, Sell with PayPal & Stripe plugin for WordPress is vulnerable to Privilege Escalation in all versions up to, and including, 30.0.2 via the `RegistryUserRole` parameter. This is due to the plugin's admin menu being registered at the `edit_posts` capability level — granting Contributor-level users access to the plugin's admin pages and a valid `cg_admin` nonce — while the option-saving handler in `change-options-and-sizes.php` performs no `current_user_can()` capability check beyond `check_admin_referer('cg_admin')`, and the `RegistryUserRole` value is processed only through `sanitize_text_field()` and `htmlentities()` without restriction to an allowlist of permitted role names. This makes it possible for authenticated attackers, with author-level access and above, to overwrite the plugin's stored `RegistryUserRole` option with `administrator`, which the `cg_create_wp_user_from_google_user` function then reads back from the `contest_gal1ery_registry_and_login_options` database table without any allowlist validation and passes directly to `wp_update_user()`, effectively promoting a newly registered Google sign-in account to Administrator.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=30.0.2
PublishedJune 16, 2026
Last updatedJune 17, 2026
Affected plugincontest-gallery

What Changed in the Fix

Changes introduced in v30.0.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This exploitation research plan targets **CVE-2026-12165**, an authenticated privilege escalation vulnerability in the **Contest Gallery** plugin (<= 30.0.2). ### 1. Vulnerability Summary The vulnerability exists due to a combination of improper capability checks and insufficient input validation. …

Show full research plan

This exploitation research plan targets CVE-2026-12165, an authenticated privilege escalation vulnerability in the Contest Gallery plugin (<= 30.0.2).

1. Vulnerability Summary

The vulnerability exists due to a combination of improper capability checks and insufficient input validation. The plugin registers its admin menu using the edit_posts capability, which is granted to users with the Contributor role and above. This allows these users to access the plugin's administration pages and retrieve a valid cg_admin nonce.

The option-saving logic (specifically handling the RegistryUserRole parameter) uses check_admin_referer('cg_admin') to prevent CSRF but fails to verify if the current user has the authority to change system-level roles (i.e., it lacks a current_user_can('manage_options') check). Furthermore, the RegistryUserRole is saved to the database without being validated against an allowlist of safe roles. An attacker can set this to administrator. When a new user registers via the Google Sign-In feature, the function cg_create_wp_user_from_google_user reads this value and promotes the new user to an Administrator.

2. Attack Vector Analysis

  • Vulnerable Endpoint: The settings update page/handler (likely admin.php?page=... involving the logic in change-options-and-sizes.php).
  • Vulnerable Parameter: options[registry-login][RegistryUserRole] (or similar POST structure processed by cg_update_registry_and_login_options_v14).
  • Authentication Level: Authenticated, Contributor role or higher (requires edit_posts capability).
  • Preconditions: The Google Sign-In feature must be available (enabled in version 30.0.2+).

3. Code Flow

  1. Entry Point: An authenticated Contributor user accesses the Contest Gallery settings in the WordPress dashboard.
  2. Nonce Retrieval: The settings page enqueues scripts/forms that include the cg_admin nonce.
  3. Option Update: The user sends a POST request to the options handler.
  4. Insecure Processing: The handler (referencing functions/general/registry/update/cg-registry-update-functions.php) calls cg_update_registry_and_login_options_v14.
    • It validates the nonce via check_admin_referer('cg_admin').
    • It does not check current_user_can('manage_options').
  5. Persistence: The RegistryUserRole value is saved into the table {prefix}contest_gal1ery_registry_and_login_options.
  6. Escalation Sink: In functions/google/cg-create-wp-user-from-google-user.php:
    • The function retrieves the role: $RegistryUserRole = $wpdb->get_var( "SELECT RegistryUserRole FROM $tablename_registry_and_login_options WHERE GeneralID = 1");
    • It creates a new user and updates their role: wp_update_user( array( 'ID' => $WpUserId, 'role' => $RegistryUserRole ) );

4. Nonce Acquisition Strategy

The cg_admin nonce is required. Since the menu is registered with edit_posts, a Contributor can access the admin pages.

  1. Log in as a Contributor.
  2. Navigate to the Contest Gallery settings page (e.g., wp-admin/admin.php?page=contest-gallery-options).
  3. Extract the nonce from the HTML or localized JS variables.
  4. Action: Use browser_eval to find the nonce if it's localized, or inspect the form for a hidden input named _wpnonce.

5. Exploitation Strategy

The goal is to modify the RegistryUserRole to administrator.

Step 1: Update the Role Option

  • Tool: http_request
  • Method: POST
  • URL: http://localhost:8080/wp-admin/admin.php?page=contest-gallery-options (Verify the exact page slug via wp menu list).
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body Parameters:
    • _wpnonce: [Extracted_cg_admin_nonce]
    • _wp_http_referer: /wp-admin/admin.php?page=contest-gallery-options
    • options[registry-login][RegistryUserRole]: administrator
    • Other required form fields (e.g., LogoutLink, BackToGalleryLink) should be sent with their current values to avoid breaking the plugin.

Step 2: Trigger Privilege Escalation (Conceptual)

  • A new user registers via Google Sign-In. The plugin will now use the malicious RegistryUserRole value to assign the 'administrator' role.

6. Test Data Setup

  1. Install Contest Gallery version 30.0.2.
  2. Create a user with the Contributor role:
    wp user create attacker attacker@example.com --role=contributor --user_pass=password
  3. Ensure the plugin is active and initialized.

7. Expected Results

  • The POST request returns a 302 Redirect or a success message.
  • The database value for RegistryUserRole in the contest_gal1ery_registry_and_login_options table is changed to administrator.

8. Verification Steps

After the HTTP request, use WP-CLI to verify the change in the custom table:

wp db query "SELECT RegistryUserRole FROM $(wp db prefix)contest_gal1ery_registry_and_login_options WHERE GeneralID = 1"

If the output is administrator, the escalation vector is confirmed.

9. Alternative Approaches

If the cg_admin nonce is not available on the main options page, check the "Registry & Login" sub-page specifically, as the vulnerability is tied to the RegistryUserRole parameter which resides there. If direct file access to change-options-and-sizes.php is possible (check for ABSPATH guards), that may be a secondary vector, though the admin menu registration is the primary intended path for this exploit.

Research Findings
Static analysis — not yet PoC-verified

Summary

The Contest Gallery plugin for WordPress is vulnerable to privilege escalation because its administration settings are accessible to users with the 'edit_posts' capability (Author level) and lack proper role validation. An attacker can overwrite the 'RegistryUserRole' setting with 'administrator', which causes the plugin to grant administrative privileges to any new user who subsequently registers through the Google Sign-In feature.

Vulnerable Code

// v10/v10-admin/options/change-options-and-sizes.php

check_admin_referer('cg_admin');

// ... 

        if($dbVersion>=14){// for new galleries after 14
            $RegistryUserRole = '';
            $RegistryUserRoleForRegistryAndLoginOptions = sanitize_text_field(htmlentities((isset($_POST['RegistryUserRole'])) ? $_POST['RegistryUserRole'] : ''));
        }else{// for older galleries before 14
            $RegistryUserRole = sanitize_text_field(htmlentities((isset($_POST['RegistryUserRole'])) ? $_POST['RegistryUserRole'] : ''));
            $RegistryUserRoleForRegistryAndLoginOptions = '';
        }

---

// functions/google/cg-create-wp-user-from-google-user.php

        $RegistryUserRole = $wpdb->get_var( "SELECT RegistryUserRole FROM $tablename_registry_and_login_options WHERE GeneralID = 1");

// ... 

            wp_update_user( array( 'ID' => $WpUserId, 'role' => $RegistryUserRole ) );

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/contest-gallery/30.0.2/v10/v10-admin/options/change-options-and-sizes.php	2026-06-11 19:34:18.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/contest-gallery/30.0.3/v10/v10-admin/options/change-options-and-sizes.php	2026-06-14 10:05:40.000000000 +0000
@@ -13,6 +13,10 @@
 //echo 'This request contained ' . count( $_POST, COUNT_RECURSIVE ) . ' POST vars, ' . count( $_GET ) . ' GET vars, and ' . count( $_COOKIE ) . ' Cookies.';
 //die;
 
+if(!is_user_logged_in() || !current_user_can('publish_posts') || !cg_user_has_backend_access()){
+    cg_die_missing_backend_access();
+}
+
 check_admin_referer('cg_admin');
 
 $id = absint($_GET['option_id']);
@@ -1236,10 +1240,24 @@
         $CheckCookieAlertMessage = (isset($_POST['CheckCookieAlertMessage'])) ? contest_gal1ery_htmlentities_and_preg_replace($_POST['CheckCookieAlertMessage']) : $CheckCookieAlertMessage;
 
         if($dbVersion>=14){// for new galleries after 14
+            $currentRegistryUserRole = $wpdb->get_var( "SELECT RegistryUserRole FROM $tablename_registry_and_login_options WHERE GeneralID = 1");
+        }else{// for older galleries before 14
+            $currentRegistryUserRole = $unsavingValues->RegistryUserRole;
+        }
+        $currentRegistryUserRole = cg_get_safe_registry_user_role($currentRegistryUserRole,$dbVersion);
+
+        if(current_user_can('manage_options')){
+            $postedRegistryUserRole = (isset($_POST['RegistryUserRole'])) ? $_POST['RegistryUserRole'] : $currentRegistryUserRole;
+            $safeRegistryUserRole = cg_get_safe_registry_user_role($postedRegistryUserRole,$dbVersion);
+        }else{
+            $safeRegistryUserRole = $currentRegistryUserRole;
+        }
+
+        if($dbVersion>=14){// for new galleries after 14
             $RegistryUserRole = '';
-            $RegistryUserRoleForRegistryAndLoginOptions = sanitize_text_field(htmlentities((isset($_POST['RegistryUserRole'])) ? $_POST['RegistryUserRole'] : ''));
+            $RegistryUserRoleForRegistryAndLoginOptions = $safeRegistryUserRole;
         }else{// for older galleries before 14
-            $RegistryUserRole = sanitize_text_field(htmlentities((isset($_POST['RegistryUserRole'])) ? $_POST['RegistryUserRole'] : ''));
+            $RegistryUserRole = $safeRegistryUserRole;
             $RegistryUserRoleForRegistryAndLoginOptions = '';
         }

Exploit Outline

The exploit requires an authenticated user with at least Author-level permissions (or any user possessing the 'edit_posts' capability). 1. Log in to the WordPress admin panel as an Author. 2. Navigate to the Contest Gallery settings page to retrieve a valid 'cg_admin' nonce (localized in JavaScript or present in the form). 3. Send a POST request to the plugin's settings handler (v10/v10-admin/options/change-options-and-sizes.php) with the following payload: - `_wpnonce`: The extracted 'cg_admin' nonce. - `RegistryUserRole`: 'administrator'. 4. This action overwrites the plugin's internal setting for default registration roles without checking if the requester has 'manage_options' capabilities. 5. To complete the escalation, the attacker (or a victim) registers a new account using the Google Sign-In feature. The plugin retrieves the malicious 'administrator' role from the database and assigns it to the new user via `wp_update_user()`.

Check if your site is affected.

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