Payment bypass: booking trusts client-supplied offering_id (no slot match) #31

Closed
opened 2026-06-09 19:08:16 +00:00 by thatguygriff · 0 comments
Owner

Severity: High — remotely exploitable by any logged-in student, direct financial impact.

Problem

BookingEndpoint::book() (src/Booking/BookingEndpoint.php:99-161) takes offering_id straight from the request and never verifies it matches the slot's own offering (or even the slot's instructor):

$offeringId = absint( $request->get_param( 'offering_id' ) );
if ( 0 === $offeringId ) { $offeringId = (int) ( $slot->offeringId ?? 0 ); }
...
if ( null !== $offering && $offering->price > 0.0 ) {
    $this->payments->createForRegistration( ..., $offering->price, ... );
}

Impact

  • A student can book a paid slot while passing the offering_id of any free offering (or one with price = 0). No Payment row is created, yet the lesson/series is reserved and markBooked() runs → free lessons.
  • A student can point at a different instructor's offering, so price and etransfer_email are sourced from an unrelated offering while instructor_id comes from the slot → payment misrouting.

Fix

Require the chosen offering to belong to $slot->instructorId and/or to be the slot's own offeringId; reject mismatches with a 400/403.

**Severity: High** — remotely exploitable by any logged-in student, direct financial impact. ## Problem `BookingEndpoint::book()` ([src/Booking/BookingEndpoint.php:99-161](src/Booking/BookingEndpoint.php#L99-L161)) takes `offering_id` straight from the request and never verifies it matches the slot's own offering (or even the slot's instructor): ```php $offeringId = absint( $request->get_param( 'offering_id' ) ); if ( 0 === $offeringId ) { $offeringId = (int) ( $slot->offeringId ?? 0 ); } ... if ( null !== $offering && $offering->price > 0.0 ) { $this->payments->createForRegistration( ..., $offering->price, ... ); } ``` ## Impact - A student can book a **paid** slot while passing the `offering_id` of any **free** offering (or one with `price = 0`). No `Payment` row is created, yet the lesson/series is reserved and `markBooked()` runs → **free lessons**. - A student can point at a **different instructor's** offering, so price and `etransfer_email` are sourced from an unrelated offering while `instructor_id` comes from the slot → **payment misrouting**. ## Fix Require the chosen offering to belong to `$slot->instructorId` and/or to be the slot's own `offeringId`; reject mismatches with a 400/403.
thatguygriff added the paymentssecuritybug labels 2026-06-09 19:08:16 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Unsupervised/unsupervised-scheduler#31