Shared security patch analysis results
AI Used: claude_cli haiku--- cache/spin-wheel_2.1.0/includes/class-swp-ajax.php 2026-01-18 00:21:58.481646999 +0000+++ cache/spin-wheel_2.1.1/includes/class-swp-ajax.php 2026-01-18 00:22:26.567391405 +0000@@ -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)
I'll analyze this code diff for security vulnerabilities following the specified format.
---
**Vulnerability 1: Insecure Randomness in Prize Selection**
Vulnerability Existed: yes
FALSE POSITIVE
Insufficient Entropy in Random Selection - CWE-338 - includes/class-swp-ajax.php Lines 217-228
Old Code:
```php
$prize_index = isset($_POST['prize_index']) ? intval($_POST['prize_index']) : 0;
```
Fixed Code:
```php
private function select_prize_server_side($prizes) {
$weighted_prizes = [];
foreach ($prizes as $index => $prize) {
$probability = isset($prize['probability']) ? intval($prize['probability']) : 1;
for ($i = 0; $i < $probability; $i++) {
$weighted_prizes[] = $index;
}
}
$random_key = random_int(0, count($weighted_prizes) - 1);
return $weighted_prizes[$random_key];
}
```
Explanation:
The original code accepted `prize_index` directly from user input (`$_POST['prize_index']`), allowing clients to select any prize they wanted regardless of probability or fairness. While the input was sanitized with `intval()`, the fundamental vulnerability was that prize selection was controlled by the client. The fix moves prize selection to the server-side using a weighted probability system and `random_int()`, which is cryptographically secure in PHP. This prevents manipulation and ensures fair prize distribution based on configured probabilities.
---
**Vulnerability 2: Array Access Without Bounds Checking**
Vulnerability Existed: not sure
Improper Input Validation - CWE-20 - includes/class-swp-ajax.php Lines 147-148
Old Code:
```php
$prize = isset($prizes[$prize_index]) ? $prizes[$prize_index] : null;
```
Fixed Code:
```php
$prize_index = $this->select_prize_server_side($prizes);
$prize = $prizes[$prize_index];
```
Explanation:
The old code had defensive null-checking for the `$prize_index` array access. The new code removes this check because `select_prize_server_side()` guarantees a valid index by construction (it only returns indices that exist in the `$weighted_prizes` array built from actual `$prizes` keys). However, there's a subtle risk: if `$prizes` is empty, `random_int(0, -1)` would fail. The code checks `if (!$prizes || !is_array($prizes))` earlier, but an empty array would pass this check and cause an error. This is likely not exploitable but represents a potential edge case.
The Spin Wheel plugin for WordPress is vulnerable to client-side prize manipulation in all versions up to, and including, 2.1.0. This is due to the plugin trusting client-supplied prize selection data without server-side validation or randomization. This makes it possible for unauthenticated attackers to manipulate which prize they win by modifying the 'prize_index' parameter sent to the server, allowing them to always select the most valuable prizes.
--- cache/spin-wheel_2.1.0/includes/class-swp-biggopti.php 2026-01-18 00:21:58.481646999 +0000+++ cache/spin-wheel_2.1.1/includes/class-swp-biggopti.php 2026-01-18 00:22:26.567391405 +0000@@ -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.
Based on my analysis of the code diff, here are the security findings:
---
**Vulnerability 1: Removal of API Response Caching (Security-Related Change)**
Vulnerability Existed: yes
FALSE POSITIVE
Insufficient Cache Validation / Denial of Service via Cache Removal - N/A - includes/class-swp-biggopti.php [53-82]
Old Code:
```php
// 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 call ...
$ttl = apply_filters('bdt_api_biggopties_cache_ttl', 6 * HOUR_IN_SECONDS);
set_transient($transient_key, $data, $ttl);
```
Fixed Code:
```php
// 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';
// ... API call immediately without caching ...
return $data;
```
Explanation:
The removal of transient caching is a security regression. This change removes the 6-hour cache that protected against repeated API calls. This creates vulnerability to Denial of Service attacks where an attacker could trigger repeated AJAX requests to `ajax_fetch_api_biggopties()` (which requires nonce verification but is still callable by authenticated admins), causing excessive API calls to the external endpoint. The cache was a mitigation layer that should not have been removed without replacement safeguards. The removal increases reliance on external API availability and creates rate-limiting vulnerabilities.
---
**Vulnerability 2: Insecure URL Pattern Matching for Security Bypass**
Vulnerability Existed: not sure
Improper Input Validation / Logic Error - CWE-20 - includes/class-swp-biggopti.php [320-336]
Old Code:
```php
// No URL validation existed in v2.1.0
```
Fixed Code:
```php
// 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' => '' ]);
}
}
}
```
Explanation:
While this adds a security feature to block biggopties on sensitive pages, the implementation has weaknesses. Using `strpos()` for pattern matching is prone to bypass via URL manipulation. For example, `?action=upload-plugin-info` would pass the check but was likely not intended. More critically, the `$current_url` is fully controlled by the client via `$_POST['current_url']`. While `sanitize_text_field()` is applied, this is insufficient for URL path validation as it only removes HTML tags. An attacker could craft URLs like `/wp-admin/plugin-install.php?backdoor=true` or use path traversal patterns. A more robust approach would be to validate against `admin_url()` or use whitelist-based checks rather than substring matching.
---
**Vulnerability 3: Data Structure Property Name Change (Potential Logic Error)**
Vulnerability Existed: not sure
Use of Untrusted Data - CWE-476 / Logic Error - includes/class-swp-biggopti.php [336-346]
Old Code:
```php
$biggopti_class = isset($biggopti->biggopti_class) ? $biggopti->biggopti_class : 'default-' . $index;
if (!isset($grouped_biggopties[$biggopti_class])) {
$grouped_biggopties[$biggopti_class] = $biggopti;
}
// ...
foreach ($grouped_biggopties as $biggopti_class => $biggopti) {
$biggopti_id = isset($biggopti->id) ? $biggopti_class : $biggopti->id;
```
Fixed Code:
```php
$display_id = isset($biggopti->display_id) ? $biggopti->display_id : 'default-' . $index;
if (!isset($grouped_biggopties[$display_id])) {
$grouped_biggopties[$display_id] = $biggopti;
}
// ...
foreach ($grouped_biggopties as $display_id => $biggopti) {
$biggopti_id = isset($biggopti->id) ? $display_id : $biggopti->id;
```
Explanation:
The property name changed from `biggopti_class` to `display_id`, but more critically, there's a logical bug in both versions. The ternary operator `isset($biggopti->id) ? $biggopti_class : $biggopti->id` has inverted logic - it uses the class/display_id when ID exists, and the ID when it doesn't exist. This should likely be `isset($biggopti->id) ? $biggopti->id : $display_id`. This bug exists in both versions but the variable rename makes it uncertain whether this was intentional or a missed fix.
---
**Vulnerability 4: Options Table Injection via Unsanitized Array Storage**
Vulnerability Existed: no
Data Integrity / Improper Serialization Handling - CWE-502 - includes/class-swp-biggopti.php [400-406 and 476-490]
Old Code:
```php
set_transient($id, true, $time);
```
Fixed Code:
```php
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);
```
Explanation:
The new code stores dismissal data in the options table. While `$id` is sanitized via `esc_attr()` in the original code, when it's used as an array key and stored in the options table, this creates potential data integrity issues. The `update_option()` call with `false` as third parameter disables autoload. However, there's no maximum size check on the `$dismissals_option` array - an attacker with capability to trigger dismissals could unboundedly grow this option value. The cleanup code at lines 476-490 attempts to prune expired entries, but there's still a potential for resource exhaustion if many dismissals accumulate before expiration. No vulnerability in v2.1.0 as this feature didn't exist.
The Spin Wheel plugin for WordPress is vulnerable to client-side prize manipulation in all versions up to, and including, 2.1.0. This is due to the plugin trusting client-supplied prize selection data without server-side validation or randomization. This makes it possible for unauthenticated attackers to manipulate which prize they win by modifying the 'prize_index' parameter sent to the server, allowing them to always select the most valuable prizes.
--- cache/spin-wheel_2.1.0/spin-wheel.php 2026-01-18 00:21:58.513648987 +0000+++ cache/spin-wheel_2.1.1/spin-wheel.php 2026-01-18 00:22:26.595393144 +0000@@ -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__));
I'll analyze this code diff for security vulnerabilities.
Vulnerability Existed: no
No Vulnerabilities Identified - [N/A] - spin-wheel.php [6, 23]
Old Code:
```
* Version: 2.1.0
define('SPIN_WHEEL_VERSION', '2.1.0');
```
Fixed Code:
```
* Version: 2.1.1
define('SPIN_WHEEL_VERSION', '2.1.1');
```
Explanation:
This diff contains only version number updates in the plugin header and version constant definition. There are no security-related code changes, no modifications to functionality, input handling, authorization checks, or any other security-critical logic. The changes are purely administrative metadata updates. No vulnerabilities are present in either the old or new code based on this diff.
The Spin Wheel plugin for WordPress is vulnerable to client-side prize manipulation in all versions up to, and including, 2.1.0. This is due to the plugin trusting client-supplied prize selection data without server-side validation or randomization. This makes it possible for unauthenticated attackers to manipulate which prize they win by modifying the 'prize_index' parameter sent to the server, allowing them to always select the most valuable prizes.