The Exploit
Attacker needs no authentication.
curl -s -X POST 'https://TARGET/wp-admin/admin-ajax.php' \
-d 'action=qsm_get_quiz_to_reload&quiz_id=123' \
-H 'Content-Type: application/x-www-form-urlencoded'
The vulnerable endpoint returns quiz data instead of 0 or an authorization error. In practice, the response contains unpublished/private quiz metadata and question markup from QSM, confirming unauthorized access to quiz details.
What the Patch Did
Before:
add_action( 'wp_ajax_qsm_get_quiz_to_reload', array( $this, 'qsm_get_quiz_to_reload' ) );
add_action( 'wp_ajax_nopriv_qsm_get_quiz_to_reload', array( $this, 'qsm_get_quiz_to_reload' ) );
After:
add_action( 'wp_ajax_qsm_get_quiz_to_reload', array( $this, 'qsm_get_quiz_to_reload' ) );
The patch removed the unauthenticated AJAX hook registration. That means admin-ajax.php?action=qsm_get_quiz_to_reload can now only be invoked by a logged-in user, restoring the WordPress AJAX authentication boundary.
Root Cause
This was a missing access-control check in WordPress AJAX registration, a CWE-862 / CWE-639 issue. The attacker-controlled action=qsm_get_quiz_to_reload parameter was accepted by the WordPress AJAX dispatcher and routed to qsm_get_quiz_to_reload() even for unauthenticated requests because the plugin registered a wp_ajax_nopriv_* callback. The trust boundary crossed unchecked was “public HTTP request → privileged quiz reload handler”, allowing unauthenticated users to read unpublished/private/password-protected quiz details.
Why It Works
The load-bearing fix is the removal of the wp_ajax_nopriv_qsm_get_quiz_to_reload hook. Without that hook, WordPress will not dispatch this AJAX action for non-logged-in users. The remaining wp_ajax_qsm_get_quiz_to_reload registration still allows authenticated users to reload quizzes as intended. The patch likely also relies on internal quiz status checks in qsm_get_quiz_to_reload() itself, but the critical security change is simply not exposing that callback to anonymous requests.
Hardening Checklist
- register anonymous AJAX endpoints only with
wp_ajax_nopriv_*when public access is explicitly required. - enforce capability checks inside callbacks with
current_user_can(), for examplecurrent_user_can('edit_qsm_quizzes'). - protect password-protected content with
post_password_required()before processing submissions or exporting quiz data. - require and validate nonces for state-changing AJAX actions using
check_ajax_referer(). - verify post status and visibility before returning quiz metadata to any AJAX request.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-9637