CVE-2025-13935

Tutor LMS – eLearning and online course solution <= 3.9.3 - Missing Authorization to Authenticated (Subscriber+) Arbitrary Course Completion

mediumMissing Authorization
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
3.9.4
Patched in
1d
Time to patch

Description

The Tutor LMS – eLearning and online course solution plugin for WordPress is vulnerable to unauthorized course completion in all versions up to, and including, 3.9.2. This is due to missing enrollment verification in the 'mark_course_complete' function. This makes it possible for authenticated attackers, with subscriber level access and above, to mark any course as completed.

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.2
PublishedJanuary 8, 2026
Last updatedJanuary 9, 2026
Affected plugintutor

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2025-13935 Tutor LMS Arbitrary Course Completion ## 1. Vulnerability Summary The **Tutor LMS** plugin (versions <= 3.9.2) contains a missing authorization vulnerability within its course completion logic. Specifically, the function `mark_course_complete` (likely lo…

Show full research plan

Exploitation Research Plan: CVE-2025-13935 Tutor LMS Arbitrary Course Completion

1. Vulnerability Summary

The Tutor LMS plugin (versions <= 3.9.2) contains a missing authorization vulnerability within its course completion logic. Specifically, the function mark_course_complete (likely located in classes/Course.php or an AJAX handler class) fails to verify if the requesting user is actually enrolled in the course they are attempting to mark as finished. Because this function is exposed via an AJAX action registered for authenticated users, any subscriber can trigger the "complete" status for any course on the platform, bypassing curriculum requirements and prerequisites.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: tutor_mark_course_complete (inferred from Tutor LMS naming conventions)
  • HTTP Method: POST
  • Parameters:
    • action: tutor_mark_course_complete
    • course_id: The ID of the target course to complete.
    • _wpnonce: A valid security nonce for the Tutor AJAX actions.
  • Authentication: Required (Subscriber level or higher).
  • Preconditions: At least one published course must exist. The attacker does not need to be enrolled in the course.

3. Code Flow

  1. Entry Point: The user sends a POST request to admin-ajax.php with the action tutor_mark_course_complete.
  2. Hook Registration: The plugin registers the action via add_action( 'wp_ajax_tutor_mark_course_complete', ... ).
  3. Nonce Verification: The handler calls check_ajax_referer() or wp_verify_nonce() using a nonce (typically named _tutor_nonce or similar).
  4. The Vulnerable Sink: The mark_course_complete function is called.
  5. Logic Gap: The function retrieves the course_id from $_POST. It checks if the user is logged in, but it omits a check like tutor_utils()->is_enrolled($course_id, $user_id).
  6. Execution: The function proceeds to record the course as completed for the current user ID in the database (typically in the {prefix}tutor_completed_course table or via user meta).

4. Nonce Acquisition Strategy

Tutor LMS localizes its security nonces into a JavaScript object globally available on pages where Tutor components are active (like the Course Archive, Single Course page, or Student Dashboard).

  1. Identify Script Localization: Tutor LMS typically uses wp_localize_script to output a configuration object named tutor_get_conf.
  2. Setup Page: Create or navigate to a page containing the Tutor Dashboard shortcode or a Course page.
  3. Extraction:
    • Page: [tutor_dashboard]
    • JavaScript Variable: window.tutor_get_conf
    • Nonce Key: nonce
  4. Action:
    • Use wp post create to ensure a page exists with [tutor_dashboard].
    • Log in as a Subscriber.
    • Use browser_navigate to that page.
    • Use browser_eval("window.tutor_get_conf?.nonce") to retrieve the token.

5. Exploitation Strategy

  1. Target Identification: Identify a Course ID (target_course_id) that the subscriber is NOT enrolled in.
  2. Nonce Retrieval: Authenticate as the subscriber and extract the nonce from the dashboard page as described above.
  3. Craft Request:
    POST /wp-admin/admin-ajax.php
    Content-Type: application/x-www-form-urlencoded
    
    action=tutor_mark_course_complete&course_id=[target_course_id]&_wpnonce=[extracted_nonce]
    
  4. Execution: Use the http_request tool to send the payload.

6. Test Data Setup

  1. Course Creation:
    wp post create --post_type=courses --post_title="Premium Course" --post_status=publish
    # Note the ID returned (e.g., 101)
    
  2. Attacker User:
    wp user create attacker attacker@example.com --role=subscriber --user_pass=password123
    
  3. Nonce Page:
    wp post create --post_type=page --post_title="Dashboard" --post_content='[tutor_dashboard]' --post_status=publish
    

7. Expected Results

  • HTTP Response: A JSON response, typically {"success": true} or a redirect/message indicating completion.
  • Database Change: A new record appearing in the wp_tutor_completed_course table (or equivalent) linking the attacker user ID to the target_course_id.

8. Verification Steps

  1. Check Completion Table:
    wp db query "SELECT * FROM wp_tutor_completed_course WHERE completed_hash_user_id = (SELECT ID FROM wp_users WHERE user_login='attacker')"
    
  2. Check via Tutor Utility (eval):
    wp eval "echo tutor_utils()->is_course_completed(101, get_user_by('login', 'attacker')->ID) ? 'COMPLETED' : 'NOT_COMPLETED';"
    
    Expected Output: COMPLETED

9. Alternative Approaches

  • Missing Nonce Check: If the _wpnonce parameter is not strictly verified or if the action tutor_mark_course_complete accepts a generic nonce, try using the REST API nonce (wp_rest) if available.
  • Different Action Name: If tutor_mark_course_complete fails, search the plugin source for "mark_course_complete" to find the exact AJAX hook name (e.g., tutor_course_complete_ajax).
  • REST API: Check if GET /wp-json/tutor/v1/course/[id]/complete exists and lacks permission callbacks.

Check if your site is affected.

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