CVE-2026-24964

Contest Gallery – Upload & Vote Photos, Media, Sell with PayPal & Stripe <= 28.1.2.1 - Authenticated (Subscriber+) Server-Side Request Forgery

mediumServer-Side Request Forgery (SSRF)
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
28.1.2.2
Patched in
10d
Time to patch

Description

The Contest Gallery – Upload & Vote Photos, Media, Sell with PayPal & Stripe plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 28.1.2.1. This makes it possible for authenticated attackers, with Subscriber-level access and above, to make web requests to arbitrary locations originating from the web application which can be used to query and modify information from internal services.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=28.1.2.1
PublishedMarch 10, 2026
Last updatedMarch 19, 2026
Affected plugincontest-gallery

What Changed in the Fix

Changes introduced in v28.1.2.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Research Plan: CVE-2026-24964 Contest Gallery SSRF ## Vulnerability Summary The **Contest Gallery** plugin (up to 28.1.2.1) is vulnerable to **Server-Side Request Forgery (SSRF)** via the `post_cg_add_openai_image` AJAX action. The vulnerability exists in the `post_cg_add_openai_image` function w…

Show full research plan

Research Plan: CVE-2026-24964 Contest Gallery SSRF

Vulnerability Summary

The Contest Gallery plugin (up to 28.1.2.1) is vulnerable to Server-Side Request Forgery (SSRF) via the post_cg_add_openai_image AJAX action. The vulnerability exists in the post_cg_add_openai_image function within functions/backend/ajax/openai/post-cg-add-openai-image.php. While the plugin attempts SSRF protection using a function called cg1l_is_safe_remote_url, this check is either insufficient or bypassable, allowing authenticated users to make arbitrary web requests. The content of the requested URL is then downloaded and saved to the WordPress uploads directory, allowing attackers to exfiltrate internal data or metadata.

Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: post_cg_add_openai_image
  • Vulnerable Parameter: cg_openai_image_url
  • Required Role: The code explicitly checks for administrator, editor, or author. However, the CVE description identifies Subscriber+ access. This discrepancy suggests that either:
    1. A Subscriber can be granted these capabilities via plugin settings (e.g., frontend upload permissions).
    2. The role check is bypassed in specific configurations.
    3. The "Contest Gallery User" role (mentioned in readme.txt) is treated as a higher-level role.
  • Preconditions:
    • The plugin must be active.
    • A valid nonce for the AJAX action must be obtained.
    • At least one Gallery must exist (to provide a valid cgGalleryID).

Code Flow

  1. Entry Point: The AJAX action wp_ajax_post_cg_add_openai_image triggers the function post_cg_add_openai_image.
  2. Nonce Check: cg_check_nonce() is called (defined elsewhere, likely validating the nonce parameter).
  3. Capability Check: The function checks if the user has administrator, editor, or author roles.
  4. Input Acquisition: The URL is retrieved from $_POST['cg_openai_image_url'].
  5. SSRF Sink:
    • If the URL is not a data URI, it passes through cg1l_is_safe_remote_url($imageUrl).
    • If it passes (or if the check is bypassed), it reaches the else block:
    $ch = curl_init($imageUrl);
    $fp = fopen($fullPath, 'wb');
    // ... curl options set ...
    curl_exec($ch);
    
  6. File Creation: The result of the curl_exec is written to $fullPath, which is located in wp-content/uploads/YYYY/MM/.

Nonce Acquisition Strategy

The plugin localizes its admin scripts into an object named cgJsClassAdmin.

  1. Identify Script Context: The OpenAI functionality is used in the "Edit Gallery" area or image editing sections.
  2. Setup: Create a standard gallery to ensure the scripts are loaded.
  3. Extraction:
    • Use wp post create to ensure a gallery or relevant page exists.
    • Navigate to the WordPress dashboard as an authenticated user.
    • Use browser_eval to find the nonce. Based on the code in post-cg-add-openai-image.php, the nonce is checked via cg_check_nonce().
    • In Contest Gallery, nonces are often found in the cg_ajax_object or the cgJsClassAdmin object.
    • Command: browser_eval("cgJsClassAdmin.nonce") or searching for wp_localize_script in the full source to find the exact key.

Exploitation Strategy

  1. Login: Authenticate as an Author (or Subscriber if permissions allow).
  2. Gallery ID: Obtain a valid Gallery ID using wp cg-gallery list (if available) or by creating one: wp post create --post_type=cg_gallery --post_title="Exploit Gallery".
  3. Trigger SSRF: Send a POST request to admin-ajax.php.
    • URL: http://localhost/wp-admin/admin-ajax.php
    • Method: POST
    • Payload:
      action=post_cg_add_openai_image
      &cg_openai_image_url=http://169.254.169.254/latest/meta-data/  (AWS Metadata)
      &cg_openai_image_name=ssrf_test
      &cgGalleryID=1
      &nonce=[NONCE]
      
  4. Retrieve Results:
    • The file will be saved as wp-content/uploads/[YEAR]/[MONTH]/ssrf_test.png.
    • Note: The plugin appends .png automatically in post-cg-add-openai-image.php.
    • Use http_request to GET the file and inspect its contents (which should contain the internal data).

Test Data Setup

  1. User Creation:
    wp user create attacker attacker@example.com --role=author --user_pass=password
  2. Gallery Creation:
    The plugin uses a custom table for galleries. Create a dummy gallery to get an ID.
    wp eval "global \$wpdb; \$wpdb->insert(\$wpdb->prefix . 'contest_gal1ery_galleries', ['title' => 'Test Gallery']);" (Check actual table name in index.php).
  3. Identify Upload Path: Use wp eval "echo wp_upload_dir()['url'];" to know where to look for the result.

Expected Results

  • The AJAX request should return a successful status (or a JS snippet updating cgJsClassAdmin).
  • A new file ssrf_test.png should appear in the current month's upload directory.
  • The content of ssrf_test.png should match the output of the internal service targeted (e.g., AWS metadata or local service HTML).

Verification Steps

  1. File Check: ls -l wp-content/uploads/$(date +%Y/%m)/ssrf_test.png
  2. Content Check: cat wp-content/uploads/$(date +%Y/%m)/ssrf_test.png
  3. Log Check: Check if curl was used to hit the internal IP in the server logs.

Alternative Approaches

  • Internal Port Scanning: Iterate cg_openai_image_url through http://localhost:[PORT] and observe timing or file creation.
  • Bypassing cg1l_is_safe_remote_url: If 127.0.0.1 is blocked, try:
    • http://0.0.0.0
    • http://[::1]
    • http://2130706433 (Decimal representation of 127.0.0.1)
    • Using a DNS entry that points to 127.0.0.1 (e.g., local.gd).
  • Data URI Bypass: The code has a specific check for data:image/. Ensure the payload is not inadvertently caught in that block.
  • Role Verification: If the Author role works, repeat the process with a Subscriber role to confirm the "Subscriber+" claim in the CVE. If it fails, check if the "Contest Gallery User" role has different behavior.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Contest Gallery plugin for WordPress is vulnerable to Server-Side Request Forgery (SSRF) via its OpenAI image integration. Authenticated attackers can use the 'post_cg_add_openai_image' AJAX action to make the server fetch arbitrary URLs, which are then saved to the public uploads directory, allowing for internal port scanning or exfiltration of sensitive metadata (like AWS/GCP instance data).

Vulnerable Code

// functions/backend/ajax/openai/post-cg-add-openai-image.php

if (empty($imageUrl) || !cg1l_is_safe_remote_url($imageUrl)) {
    // ... error handling ...
} else {
    // Initialize cURL session
    $ch = curl_init($imageUrl);
    $fp = fopen($fullPath, 'wb');

    $default_socket_timeout = (int)(ini_get('default_socket_timeout'));
    $max_execution_time = (int)(ini_get('max_execution_time'));

    curl_setopt($ch, CURLOPT_FILE, $fp);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, ($default_socket_timeout - 1));
    curl_setopt($ch, CURLOPT_TIMEOUT, ($max_execution_time - 1));
    curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    curl_close($ch);
    fclose($fp);
    // ... (truncated)

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/contest-gallery/28.1.2.1/functions/backend/ajax/openai/post-cg-add-openai-image.php	2026-01-06 11:32:58.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/contest-gallery/28.1.2.2/functions/backend/ajax/openai/post-cg-add-openai-image.php	2026-01-12 16:50:46.000000000 +0000
@@ -139,7 +140,8 @@
                         imagepng($formImage,$fullPath);
                     }else{
                         // Initialize cURL session
-                        $ch = curl_init($imageUrl);
+                        /*$ch = curl_init($imageUrl);
+
                         $fp = fopen($fullPath, 'wb');
 
                         $default_socket_timeout = (int)(ini_get('default_socket_timeout'));
@@ -179,8 +181,62 @@
                             //]);
                             $cgAddOpenAiImageErrorMessage = 'Failed to download image: ' . ($error ?: 'HTTP code ' . $httpCode);
                             $cgAddOpenAiImageIsValid = false;
+                        }*/
+
+                        // Timeouts berechnen (wie in deinem Original)
+                        // Calculate timeouts based on server settings
+                        $default_socket_timeout = (int)(ini_get('default_socket_timeout'));
+                        $max_execution_time = (int)(ini_get('max_execution_time'));
+                        $ct = $default_socket_timeout > 2 ? ($default_socket_timeout - 1) : 10;
+                        $to = $max_execution_time > 2 ? ($max_execution_time - 1) : 20;
+
+                        // Path security: Prevent Directory Traversal attacks
+                        if (empty($fullPath) || strpos($fullPath, '..') !== false) {
+                            $cgAddOpenAiImageErrorMessage = 'Security Error: Invalid file path.';
+                            $cgAddOpenAiImageIsValid = false;
+                        } else {
+                            // wp_safe_remote_get replaces cURL and provides native SSRF protection as requested by Patchstack
+                            // if dynamic url from unknown source, then better to use wp_safe_remote_get
+                            $response = wp_safe_remote_get($imageUrl, [
+                                'timeout'     => $to, // WordPress uses a single combined timeout
+                                'redirection' => 0,   // Equivalent to CURLOPT_FOLLOWLOCATION => false (SSRF Hardening)
+                                'sslverify'   => true // Equivalent to CURLOPT_SSL_VERIFYPEER
+                            ]);
+
+                            if (is_wp_error($response)) {
+                                $cgAddOpenAiImageErrorMessage = 'Failed to download image: ' . $response->get_error_message();
+                                $cgAddOpenAiImageIsValid = false;
+                            } else {
+                                $httpCode   = wp_remote_retrieve_response_code($response);
+                                $image_data = wp_remote_retrieve_body($response);
+
+                                if ($httpCode !== 200 || empty($image_data)) {
+                                    $cgAddOpenAiImageErrorMessage = 'Failed to download image: HTTP code ' . $httpCode;
+                                    $cgAddOpenAiImageIsValid = false;
+                                } else {
+                                    // Content Validation: Verify the actual file content (MIME-Type)
+                                    $is_valid_image = false;
+
+                                    if (class_exists('finfo')) {
+                                        $finfo = new finfo(FILEINFO_MIME_TYPE);
+                                        $mime_type = $finfo->buffer($image_data);
+                                        $is_valid_image = in_array($mime_type, ['image/png', 'image/jpeg', 'image/jpg', 'image/webp']);
+                                    } else {
+                                        // Fallback: Use getimagesizefromstring if finfo extension is missing
+                                        $image_info = getimagesizefromstring($image_data);
+                                        $is_valid_image = ($image_info !== false);
                         }
 
+                                    if (!$is_valid_image) {
+                                        $cgAddOpenAiImageErrorMessage = 'Invalid file type. Only PNG, JPG, and WEBP are allowed.';
+                                        $cgAddOpenAiImageIsValid = false;
+                                    } else {
+                                        // Securely save the validated data to the disk
+                                        file_put_contents($fullPath, $image_data);
+                                    }
+                                }
+                            }
+                        }
                     }

Exploit Outline

1. Authenticate to the WordPress site (defaults to Author role, though Subscriber might be possible depending on plugin configurations). 2. Obtain a valid AJAX nonce, typically found in the `cgJsClassAdmin` or `cg_ajax_object` JavaScript variables localized on admin pages like the Gallery editor. 3. Send a POST request to `/wp-admin/admin-ajax.php` with the following parameters: - `action`: `post_cg_add_openai_image` - `cg_openai_image_url`: The target internal URL (e.g., `http://169.254.169.254/latest/meta-data/` or a local service). - `cg_openai_image_name`: A chosen filename (e.g., `ssrf_result`). - `cgGalleryID`: A valid ID for an existing gallery. - `nonce`: The acquired security nonce. 4. The plugin will fetch the target URL and save it to the current month's upload directory (e.g., `wp-content/uploads/2025/01/ssrf_result.png`). 5. Access the created file via its URL to view the exfiltrated content.

Check if your site is affected.

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