CVE-2026-3225

LearnPress <= 4.3.2.8 - Missing Authorization to Authenticated (Subscriber+) Arbitrary Quiz Answer Deletion

mediumMissing Authorization
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
4.3.3
Patched in
1d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=4.3.2.8
PublishedMarch 23, 2026
Last updatedMarch 23, 2026
Affected pluginlearnpress

What Changed in the Fix

Changes introduced in v4.3.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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 (The AbstractAjax dispatcher triggers based on the lp-ajax query 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 the learnpress_question_answers database 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 QuestionAnswerModel minimum count check).

3. Code Flow

  1. Entry Point: A request is made to POST /?lp-ajax=delete_question_answer.
  2. Dispatcher: LearnPress\REvent\Ajax\AbstractAjax::catch_lp_ajax() (inferred namespace) is executed (hooked early, likely on init or wp_loaded).
  3. Nonce Validation: The dispatcher verifies the _wpnonce parameter against the wp_rest action using wp_verify_nonce().
  4. Missing Auth Check: The dispatcher proceeds to call the method corresponding to the lp-ajax parameter value (delete_question_answer) without verifying if the current user has permissions like edit_lp_questions.
  5. Vulnerable Method: LearnPress\REvent\Ajax\EditQuestionAjax::delete_question_answer() is called.
  6. Sink: LearnPress\Models\QuestionAnswerModel::delete() is called with the provided question_id and answer_id, performing the deletion in the learnpress_question_answers table.

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.

  1. Login: Log in as a Subscriber user.
  2. Navigate: Navigate to the WordPress home page or any page where LearnPress scripts are loaded.
  3. Extract: Use browser_eval to extract the nonce from the lpData global JavaScript object, which LearnPress uses to store configuration data.
    • JS Command: window.lpData?.nonce or window.lpGlobalSettings?.nonce.
    • If not found in lpData, the standard WordPress REST nonce is often available at window.wpApiSettings?.nonce.

5. Exploitation Strategy

  1. 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 the answer_id of one option (from the wp_learnpress_question_answers table).
  2. Authentication: Log in as a Subscriber.
  3. Nonce Retrieval: Execute browser_eval to obtain the wp_rest nonce.
  4. Execution: Send a POST request 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

  1. Plugin Configuration: Ensure LearnPress is active.
  2. 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;
    "
    
  3. 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'"
    
  4. 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 simple 1.
  • Side Effect: The row in wp_learnpress_question_answers corresponding to the targeted answer_id must be removed.

8. Verification Steps

  1. Database Check: After the http_request, verify the answer is deleted using WP-CLI.
    wp db query "SELECT * FROM wp_learnpress_question_answers WHERE question_answer_id = [ANSWER_ID]"
    
    Expected: No rows returned.
  2. 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.php with action=lp_ajax&lp-ajax=delete_question_answer.
  • JSON Payload: Some LP 4.x endpoints expect JSON. Try setting Content-Type: application/json and sending {"question_id": ..., "answer_id": ..., "_wpnonce": ...}.
  • REST Nonce Header: If _wpnonce in the body is rejected, send the nonce via the X-WP-Nonce header.
Research Findings
Static analysis — not yet PoC-verified

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

--- a/inc/REvent/Ajax/EditQuestionAjax.php
+++ b/inc/REvent/Ajax/EditQuestionAjax.php
@@ -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.