CVE-2026-3358

Tutor LMS <= 3.9.7 - Missing Authorization to Authenticated (Subscriber+) Unauthorized Private Course Enrollment

mediumMissing Authorization
5.4
CVSS Score
5.4
CVSS Score
medium
Severity
3.9.8
Patched in
1d
Time to patch

Description

The Tutor LMS – eLearning and online course solution plugin for WordPress is vulnerable to unauthorized private course enrollment in all versions up to, and including, 3.9.7. This is due to missing post_status validation in the `enroll_now()` and `course_enrollment()` functions. Both enrollment endpoints verify the nonce, user authentication, and whether the course is purchasable, but fail to check if the course has a `private` post_status. This makes it possible for authenticated attackers with Subscriber-level access or above to enroll in private courses by sending a crafted POST request with the target course ID. The enrollment record is created in the database and the private course title and enrollment status are exposed in the subscriber's dashboard, though WordPress core access control prevents the subscriber from viewing the actual course content (returns 404). Enrollment in private courses should be restricted to users with the `read_private_posts` capability.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=3.9.7
PublishedApril 10, 2026
Last updatedApril 11, 2026
Affected plugintutor

What Changed in the Fix

Changes introduced in v3.9.8

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This analysis is based on the provided vulnerability description and the architectural patterns of the **Tutor LMS** plugin. Since the core PHP logic files were not provided in the snippet, the specific function locations and hooks are grounded in the CVE description and established Tutor LMS source…

Show full research plan

This analysis is based on the provided vulnerability description and the architectural patterns of the Tutor LMS plugin. Since the core PHP logic files were not provided in the snippet, the specific function locations and hooks are grounded in the CVE description and established Tutor LMS source patterns.

1. Vulnerability Summary

The vulnerability is a Missing Authorization (specifically, missing object status validation) within the enrollment logic of Tutor LMS. When a user attempts to enroll in a course, the functions enroll_now() and course_enrollment() (likely located in classes/Course.php or similar logic handlers) verify that the user is authenticated and that the request is not a Cross-Site Request Forgery (CSRF) via a nonce check.

However, they fail to verify the post_status of the target course_id. In WordPress, "Private" posts are intended to be hidden from everyone except those with the read_private_posts capability (typically Admins and Editors). By failing to check if a course is private, the plugin allows any authenticated user (Subscriber level) to create an enrollment record for a restricted course, effectively "joining" a class they should not even be able to see.

2. Attack Vector Analysis

  • Endpoint: WordPress AJAX interface (/wp-admin/admin-ajax.php) or the admin-post.php handler.
  • Action: The likely AJAX action is tutor_enroll_now or a direct POST to the enrollment handler.
  • Parameters:
    • course_id: (Integer) The ID of the private course.
    • tutor_nonce or _wpnonce: (String) A valid nonce for the enrollment action.
  • Authentication: Subscriber-level (or any authenticated user) is required.
  • Preconditions: A course must exist with the post_status set to private.

3. Code Flow (Inferred)

  1. Entry Point: User triggers an enrollment request, usually via a POST request to admin-ajax.php with action=tutor_enroll_now.
  2. Nonce Verification: The handler calls check_ajax_referer('tutor_nonce', 'tutor_nonce') or wp_verify_nonce().
  3. Authentication Check: The handler verifies is_user_logged_in().
  4. Purchasability Check: The code calls a helper like tutor_utils()->is_course_purchasable($course_id). For many "Private" courses that are not connected to WooCommerce/EDD, this may return false (meaning it's free/direct), allowing the flow to proceed.
  5. The Flaw: The code proceeds to call tutor_utils()->do_enroll($course_id) without checking if get_post_status($course_id) === 'private'.
  6. Sink: A record is inserted into the {wpdb->prefix}tutor_enrolled database table.

4. Nonce Acquisition Strategy

Tutor LMS localizes its security tokens into a global JavaScript object available on most frontend pages where Tutor LMS components are active (e.g., the Dashboard or Course Archive).

  • Localization Key: tutor_get_conf
  • Nonce Path: tutor_get_conf.nonce
  • Strategy:
    1. Identify Trigger: The [tutor_dashboard] shortcode is the most reliable way to ensure all Tutor LMS frontend scripts and nonces are loaded.
    2. Setup Page: Create a temporary page containing this shortcode.
    3. Acquisition:
      • Log in as a Subscriber.
      • Navigate to the created page.
      • Execute: browser_eval("tutor_get_conf.nonce").

5. Exploitation Strategy

This plan uses the http_request tool to simulate a Subscriber enrolling in a Private course.

Step 1: Discover/Identify Private Course ID
The attacker needs the ID of a private course. In a test environment, this is created during setup. In a real scenario, this might be found through ID enumeration or information leaks.

Step 2: Obtain Nonce
As the Subscriber, navigate to the Tutor Dashboard and extract the nonce.

Step 3: Execute Unauthorized Enrollment
Perform a POST request to the AJAX endpoint.

  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    action=tutor_enroll_now&course_id=[PRIVATE_COURSE_ID]&tutor_nonce=[EXTRACTED_NONCE]
    

6. Test Data Setup

To be performed via wp_cli:

  1. Create Private Course:
    wp post create --post_type=courses --post_title="Top Secret Course" --post_status=private --post_author=1
    # Note the resulting ID (e.g., 123)
    
  2. Create Subscriber User:
    wp user create attacker attacker@example.com --role=subscriber --user_pass=password
    
  3. Create Nonce Source Page:
    wp post create --post_type=page --post_title="Dashboard" --post_status=publish --post_content='[tutor_dashboard]'
    

7. Expected Results

  • HTTP Response: The server should return a JSON success message (e.g., {"success":true,...}) or a redirect to the course page.
  • Dashboard Exposure: If the attacker visits their Subscriber dashboard, the "Top Secret Course" will now appear in their "Enrolled Courses" list.
  • Database State: A new entry will exist in the wp_tutor_enrolled table linking the attacker user ID to the private course ID.

8. Verification Steps

After the exploit attempt, verify the unauthorized enrollment via wp_cli:

# Check the tutor_enrolled table directly
wp db query "SELECT * FROM wp_tutor_enrolled WHERE course_id = [PRIVATE_COURSE_ID];"

# Verify if the user (ID 2) is now enrolled
wp db query "SELECT user_id, course_id FROM wp_tutor_enrolled WHERE user_id = 2 AND course_id = [PRIVATE_COURSE_ID];"

If a row exists for the Subscriber user and the Private course, the exploitation is confirmed.

9. Alternative Approaches

If the tutor_enroll_now AJAX action is restricted or behaves differently in the specific version:

  1. Direct POST: Try sending the request directly to a course URL with tutor_enroll_now=true and the required nonce and ID as query parameters or POST body.
  2. Course Enrollment Action: Check for the course_enrollment action (mentioned in the CVE description), which might be tied to a different hook like admin_post_tutor_course_enrollment.
  3. Variable Guessing: If tutor_nonce fails, check the localized JS in assets/js/lazy-chunks/ for other potential nonce identifiers used in the enrollment flow. (e.g., searching for nonce in tutor-course-builder-basic.js or similar frontend logic).

Check if your site is affected.

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