Introduction
A missing authorization check in the LearnPress WordPress plugin lets any unauthenticated visitor delete arbitrary quiz answer options. The flaw is tracked as CVE-2026-4365 with a CVSS score of 9.1, and it can silently corrupt course assessments across more than 100,000 active installations. The only token an attacker needs is a wp_rest nonce, and the plugin itself embeds that nonce in public HTML.
LearnPress, developed by ThimPress, is one of the most popular open-source Learning Management System (LMS) plugins for WordPress. It powers online course creation, quizzes, and student management for educational institutions, corporate training platforms, and independent instructors worldwide. At over 100,000 active installations, a LearnPress vulnerability has a broad blast radius across the WordPress ecosystem.
Technical Information
The vulnerability is classified under CWE-862 (Missing Authorization). It comes from an architectural oversight in which three separate components chain together into an unauthenticated path for deleting data.
The Nonce Exposure Problem
LearnPress embeds a wp_rest nonce directly into the lpData JavaScript object, which is rendered in the public frontend HTML of every page where the plugin is active. Any visitor can see this nonce, authenticated or not. The relevant code is in class-lp-assets.php at line 177.
In standard WordPress security practice, nonces are paired with capability checks so that a request gets both CSRF protection and authorization. A nonce alone does not prove a user is authorized to perform an action. It only proves the request came from a page that WordPress rendered. By exposing this nonce publicly, LearnPress effectively neutralizes its only security gate.
The AJAX Dispatcher
The lp-load-ajax dispatcher, implemented in AbstractAjax.php at line 33, accepts incoming AJAX requests and routes them to the appropriate handler. The dispatcher validates the wp_rest nonce but does no other authentication or capability check. Because the nonce is public, any visitor passes this validation trivially.
The Vulnerable Action
The delete_question_answer action, found in EditQuestionAjax.php at line 285, is what deletes quiz answer options. The function has no capability check and no ownership check. It accepts a question answer ID and deletes that answer directly.
Attack Flow
The exploitation path is straightforward:
- An attacker visits any public page on a WordPress site running LearnPress (versions up to and including 4.3.2.8).
- The attacker inspects the page source or JavaScript context to extract the
wp_restnonce from thelpDataobject. - The attacker crafts a POST request to the
lp-load-ajaxendpoint, specifying thedelete_question_answeraction and including the extracted nonce along with the target answer ID. - The AJAX dispatcher validates the nonce (which passes, since WordPress legitimately generated it) and routes the request to the handler.
- The
delete_question_answerfunction runs the deletion without checking whether the requester has any WordPress capabilities or any relationship to the question. - The targeted quiz answer option is permanently removed from the database.
The following table summarizes how the three components interact:
| Component | Location | Flaw | Security Implication |
|---|---|---|---|
lpData object | class-lp-assets.php | Exposes wp_rest nonce in public HTML | Provides attackers the required token |
lp-load-ajax dispatcher | AbstractAjax.php | Uses nonce as the sole security gate | Bypasses standard authentication |
delete_question_answer | EditQuestionAjax.php | No capability or ownership check | Allows arbitrary answer deletion |
The attacker needs only a browser and a simple HTTP client, yet deleting quiz answers can silently corrupt assessments, invalidate student results, and undermine the integrity of entire course catalogs. That asymmetry between effort and damage is what makes this vulnerability so serious.
Patch Information
The vulnerability was patched in LearnPress version 4.3.3, released to the WordPress plugin repository. The fix is a surgical, single-line addition inside the QuestionAnswerModel class at inc/Models/Question/QuestionAnswerModel.php.
In version 4.3.2.8 and earlier, the delete() method calls check_valid_before_delete() as its gatekeeper. That pre-delete validation checked only two things: whether the parent question existed, and whether the question type still had the minimum number of answers. It did no authorization check at all.
The model class already had a check_capabilities_update() method, and the save() method used it properly for create and update operations. That method calls WordPress's user_can() to verify that the current user holds the edit_lp_lesson capability for the specific question. The delete path never called it.
In version 4.3.3, the developers added a single call to $this->check_capabilities_update() inside check_valid_before_delete(), right after the question existence check and before the answer count validation:
public function check_valid_before_delete() { $questionPostModel = $this->get_question_post_model(); if ( ! $questionPostModel ) { throw new Exception( __( 'Question not found', 'learnpress' ) ); } + + $this->check_capabilities_update(); if ( $questionPostModel->get_type() === 'single_choice' || ...
The check_capabilities_update() method itself enforces the authorization:
public function check_capabilities_update() { $user = wp_get_current_user(); if ( ! user_can( $user, 'edit_' . LP_LESSON_CPT, $this->question_id ) ) { throw new Exception( __( 'You do not have permission to edit this item.', 'learnpress' ) ); } }
After the patch, an unauthenticated attacker can still send a crafted POST request to the delete_question_answer AJAX action with the publicly exposed wp_rest nonce, but the request now fails. The check_capabilities_update() call retrieves the current user, which will be an anonymous visitor with no capabilities. The user_can() check fails, and the method throws an exception that halts the deletion before it reaches the database.
The developers also bumped the method's PHPDoc version tag from @version 1.0.0 to @version 1.0.1, which confirms this was a targeted, intentional security fix. The patch is elegant because it reuses the check_capabilities_update() authorization method that already protected the save() flow. Leaving the delete path unguarded was an oversight.
Organizations running LearnPress should update to version 4.3.3 or later immediately. You can compare the vulnerable and patched model files directly in the WordPress plugin SVN repository.
Affected Systems and Versions
All versions of the LearnPress WordPress plugin up to and including 4.3.2.8 are affected. The vulnerability is present in any WordPress installation where LearnPress is active, regardless of WordPress core version or server configuration.
The fix is available in LearnPress version 4.3.3.
Vendor Security History
LearnPress has a documented pattern of security vulnerabilities, and organizations that rely on the plugin should pay attention to it:
- An authenticated SQL injection via the
orderparameter, reachable by users with Contributor-level access, affected versions up to 4.2.6.9.3. It was patched in version 4.2.6.9.4. - CVE-2026-4333, a stored cross-site scripting (XSS) vulnerability, was recently identified in LearnPress. It points to the same recurring gaps in input validation and authorization.
This history of authorization bypasses, injection flaws, and XSS issues suggests that organizations deploying LearnPress should monitor it strictly, apply updates promptly, and consider additional hardening such as Web Application Firewalls with WordPress-specific rulesets.
References
- Wordfence Vulnerability Advisory for CVE-2026-4365
- NVD Entry for CVE-2026-4365
- AbstractAjax.php (AJAX Dispatcher Source)
- EditQuestionAjax.php (Vulnerable Action Source)
- class-lp-assets.php (Nonce Exposure Source)
- QuestionAnswerModel.php (Version 4.3.2.8, Vulnerable)
- QuestionAnswerModel.php (Version 4.3.3, Patched)
- LearnPress on WordPress.org Plugin Directory
- LearnPress Official Changelog



