The Exploit
An unauthenticated attacker can request sensitive booking data by sending a direct REST API call to the EventPrime booking endpoints without any authentication token or WordPress session.
curl -X GET "http://target.local/wp-json/eventprime/v1/bookings" \
-H "Content-Type: application/json"
The server responds with a 200 status and returns a JSON array containing all booking records, including customer names, email addresses, payment amounts, and event details. An attacker observes full booking data exposure without presenting any credentials. By iterating the booking ID parameter, the attacker enumerates and exfiltrates the entire booking database.
What the Patch Did
Before:
'permission_callback' => array( $this, 'permission_callback' ),
After:
'permission_callback' => array( $this, 'bookings_permission_callback' ),
The patch replaced a generic, inadequate permission callback with a specialized bookings_permission_callback() method that enforces three sequential capability checks: API token validation via ep_require_token_or_401(), WordPress user authentication via is_user_logged_in(), and post editing capability via current_user_can('edit_posts'). The vulnerable code used a permission callback that did not adequately validate user authorization before granting access to booking endpoints. This control — the bookings_permission_callback() method — is the load-bearing security gate that the patch introduced.
Root Cause
CWE-862: Missing Authorization — Broken Access Control on REST API endpoints.
The EventPrime plugin registered multiple REST API endpoints for booking operations (/eventprime/v1/bookings, /eventprime/v1/bookings/{id}, and related operations) without proper capability checks. The vulnerable code assigned a generic permission_callback that either performed no validation or performed insufficient validation, allowing unauthenticated and unauthorized users to reach the endpoint handlers. The request parameter wp-json/eventprime/v1/bookings passes directly into the REST router; the router invokes the permission callback; the callback fails to reject the request; the handler executes and returns sensitive data. No trust boundary — WordPress user authentication — was enforced before the sensitive data sink was reached.
Why It Works
The single load-bearing line is 'permission_callback' => array( $this, 'bookings_permission_callback' ) — the assignment of the correct callback function. If this line were removed and the old generic callback remained, the bug would still be exploitable because the original callback does not validate user authentication or booking-related capabilities. The engineer added the additional is_user_logged_in() and current_user_can('edit_posts') checks within the new callback to implement defence-in-depth: the token check protects API-only access patterns; the is_user_logged_in() check ensures a WordPress user session exists; and the current_user_can('edit_posts') check enforces a minimum capability boundary, preventing editors, authors, and subscribers from accessing booking data. Each layer independently rejects unauthorized requests — removing any one layer still provides some protection, but the combination ensures that compromise of one authentication mechanism (e.g., leaked API token) does not automatically grant booking access.
Hardening Checklist
-
Audit all REST endpoint registrations: Search the codebase for
register_rest_route()calls and verify each one assigns a named, specific permission callback — not a generic or null callback. Usegrep -r "register_rest_route"and manually inspect the'permission_callback'parameter. -
Implement per-resource capability checks: Define resource-specific permission callbacks (e.g.,
bookings_permission_callback(),tickets_permission_callback()) that callcurrent_user_can()with resource-specific capabilities — not blanket checks likeis_user_logged_in()alone. -
Validate and reject invalid REST responses in permission checks: Ensure permission callbacks return either
true(allow) or aWP_Errorobject with a 403 or 401 status. Never returnnull,false, or aWP_REST_Responseobject directly, as these may bypass proper error standardization. Usenew WP_Error( 'rest_forbidden', $message, array( 'status' => 403 ) ). -
Implement consistent error response handling: Wrap permission check results in a conditional that detects
WP_REST_Responseinstances and converts them to standardizedWP_Errorobjects. This prevents information leakage through inconsistent HTTP status codes and ensures all access denials are logged uniformly. -
Test unauthenticated access to all endpoints: Write automated tests that attempt to call each REST endpoint without authentication tokens, session cookies, or
current_user_can()capabilities. Assert that all responses return 401 or 403 status codes. Use WordPress PHPUnit or a CI pipeline REST client to enforce this check on every release.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-69358