CVE-2026-23799

Tutor LMS – eLearning and online course solution <= 3.9.5 - Missing Authorization

mediumMissing Authorization
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
3.9.6
Patched in
9d
Time to patch

Description

The Tutor LMS – eLearning and online course solution plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in all versions up to, and including, 3.9.5. This makes it possible for authenticated attackers, with Subscriber-level access and above, to perform an unauthorized action.

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<=3.9.5
PublishedFebruary 25, 2026
Last updatedMarch 5, 2026
Affected plugintutor

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan focuses on **CVE-2026-23799**, a Missing Authorization vulnerability in **Tutor LMS** (<= 3.9.5). The vulnerability allows a Subscriber-level user to execute privileged actions because a capability check is missing in an AJAX handler. ### 1. Vulnerability Summary The vulnerabilit…

Show full research plan

This research plan focuses on CVE-2026-23799, a Missing Authorization vulnerability in Tutor LMS (<= 3.9.5). The vulnerability allows a Subscriber-level user to execute privileged actions because a capability check is missing in an AJAX handler.

1. Vulnerability Summary

The vulnerability exists in the plugin's handling of AJAX requests. Tutor LMS often uses a centralized AJAX dispatcher (frequently named tutor_ajax) or specific AJAX hooks (wp_ajax_tutor_*). The affected versions fail to verify if the authenticated user has the necessary permissions (e.g., tutor_instructor or manage_options) before executing logic that modifies database state, such as managing announcements, quizzes, or course settings.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: tutor_ajax (dispatching to a vulnerable tutor_action) or a direct wp_ajax_ hook.
  • Vulnerable Parameter: tutor_action (if using the dispatcher) or specific parameters related to the action (e.g., course_id, announcement_id).
  • Authentication: Subscriber-level access is required.
  • Preconditions: The attacker must be logged in and obtain a valid Tutor LMS AJAX nonce.

3. Code Flow (Inferred)

  1. Entry Point: The plugin registers AJAX handlers in classes/Ajax.php or classes/Init.php using add_action( 'wp_ajax_tutor_ajax', ... ).
  2. Dispatcher: The handler function (likely Tutor\Models\Ajax::tutor_ajax) retrieves the tutor_action from the $_POST request.
  3. Missing Check: The dispatcher checks the WordPress nonce (via check_ajax_referer or wp_verify_nonce) but fails to call current_user_can() to verify the user's role.
  4. Action Execution: The dispatcher calls a specific function (e.g., tutor_announcement_create_or_update) that performs the privileged operation.
  5. Sink: Database modification occurs via $wpdb or update_post_meta.

4. Nonce Acquisition Strategy

Tutor LMS localizes its security tokens into the tutor_get_conf JavaScript object. To obtain a valid nonce as a Subscriber:

  1. Setup: Ensure a course exists and the Subscriber is logged in.
  2. Navigation: Navigate the browser to any Tutor LMS dashboard page or course page where the plugin's scripts are loaded.
  3. Extraction:
    • JS Variable: tutor_get_conf
    • Nonce Key: nonce
    • Command: browser_eval("window.tutor_get_conf?.nonce")
  4. Fallback: If not in tutor_get_conf, check for _tutor_nonce in the global window scope or search the HTML source for name="_tutor_nonce".

5. Exploitation Strategy

We will target an action that allows a Subscriber to create a Course Announcement, an action normally reserved for Instructors or Admins.

  • Step 1: Log in as a Subscriber.
  • Step 2: Obtain the nonce using the method in Section 4.
  • Step 3: Identify a target course_id.
  • Step 4: Send a crafted AJAX request to create an announcement.

HTTP Request (Example):

  • Method: POST
  • URL: https://target.site/wp-admin/admin-ajax.php
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    action=tutor_ajax&tutor_action=tutor_announcement_create_or_update&nonce=[NONCE]&course_id=[COURSE_ID]&announcement_title=Exploited&announcement_content=Unauthorized+Announcement
    

6. Test Data Setup

  1. Create Admin: Already exists in environment.
  2. Create Subscriber:
    wp user create attacker attacker@example.com --role=subscriber --user_pass=password
  3. Create Course:
    wp post create --post_type=courses --post_title="Target Course" --post_status=publish --post_author=1
    (Note: Capture the generated Post ID as COURSE_ID).
  4. Verify Plugin State: Ensure tutor plugin is active and version is <= 3.9.5.

7. Expected Results

  • Response: The server should return a JSON success message (e.g., {"success":true,...}).
  • Impact: A new announcement record is created in the database, associated with the specified course_id, despite the requester being a Subscriber.

8. Verification Steps

After the HTTP request, verify the creation of the announcement via WP-CLI:

  1. Check for new post/comment: Tutor LMS often stores announcements as comments or custom post types.
    • Check comments: wp comment list --post_id=[COURSE_ID]
    • Check for specific metadata: wp post meta list [COURSE_ID]
  2. Check Database directly:
    wp db query "SELECT * FROM wp_posts WHERE post_title = 'Exploited' AND post_type = 'tutor_announcements'" (or relevant table).

9. Alternative Approaches

If tutor_announcement_create_or_update is not the specific vulnerable action, try:

  • tutor_place_order: Attempt to enroll the subscriber in a course.
  • tutor_mark_answer_as_correct: Attempt to modify quiz results.
  • tutor_quiz_builder_get_question_form: Access internal quiz structures.

If the tutor_ajax dispatcher is not the entry point, look for specific AJAX hooks like wp_ajax_tutor_save_quiz_answer_options and test them directly using action=tutor_save_quiz_answer_options.

Research Findings
Static analysis — not yet PoC-verified

Summary

Tutor LMS version 3.9.5 and earlier fails to perform proper authorization checks in its centralized AJAX dispatcher or specific AJAX handlers. This allows authenticated users with Subscriber-level permissions to execute restricted functions, such as creating course announcements or modifying quiz settings, provided they possess a valid security nonce.

Vulnerable Code

// tutor/classes/Ajax.php (Approximate logic in vulnerable versions)

public function tutor_ajax() {
    // Nonce check is usually present
    check_ajax_referer('tutor_nonce', 'nonce');

    $action = isset($_POST['tutor_action']) ? sanitize_text_field($_POST['tutor_action']) : '';

    // MISSING: current_user_can() check to ensure the user is an instructor or admin

    if (method_exists($this, $action)) {
        $this->$action();
    } elseif (is_callable($action)) {
        call_user_func($action);
    }
    
    wp_die();
}

Security Fix

--- tutor/classes/Ajax.php
+++ tutor/classes/Ajax.php
@@ -10,6 +10,11 @@
     public function tutor_ajax() {
         check_ajax_referer('tutor_nonce', 'nonce');
 
+        // Added capability check to prevent unauthorized access
+        if ( ! current_user_can('tutor_instructor') && ! current_user_can('manage_options') ) {
+            wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
+        }
+
         $action = isset($_POST['tutor_action']) ? sanitize_text_field($_POST['tutor_action']) : '';
 
         if (method_exists($this, $action)) {

Exploit Outline

1. Authentication: Log in to the WordPress site as a Subscriber-level user. 2. Nonce Acquisition: Access the dashboard or any page where Tutor LMS scripts are loaded and extract the AJAX nonce from the JavaScript object `tutor_get_conf.nonce` or the `_tutor_nonce` HTML input. 3. Identify Target Action: Choose a privileged Tutor LMS action typically restricted to instructors, such as 'tutor_announcement_create_or_update'. 4. Payload Construction: Craft a POST request to `/wp-admin/admin-ajax.php` with the parameters: `action=tutor_ajax`, `tutor_action=[TARGET_ACTION]`, `nonce=[NONCE]`, and action-specific data (e.g., `course_id`, `announcement_title`). 5. Execution: Submit the request. Since the plugin fails to verify user capabilities beyond the nonce, the server will execute the privileged logic on behalf of the Subscriber.

Check if your site is affected.

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