Forums Bug Reports Thread

Unauthenticated course-purchase success page leaks buyer email by Stripe session id (IDOR / broken access control)

Patrick Bass · Jun 6 · 15 · 1 Locked
[Major] [High Priority] [Bug Fixed] [Always Reproduces]
🚀 OP Jun 6, 2026 7:06pm

Area: Monetization (audit p10) · Surface: GET /learn/enrol/success (CourseCatalogController@success) · Dimension: security · Severity: major

Anyone who obtains a Stripe Checkout session id (the `cs=cs_live_...` value appears in success-redirect URLs, browser history, Referer headers sent to third-party assets, and proxy/analytics logs) can hit /learn/enrol/success?cs=<id> while logged out and view the buyer's email address. Because the handler also drives enrolment + the confirmation email on the `pending`->`paid` transition (lines 270-298), a third party replaying the URL can also trigger enrolment side-effects for the legitimate buyer. Session ids are long/unguessable so this is not a mass-enumeration hole, but it is a real cross-user information disclosure with no authentication or ownership binding on a monetization surface that handles PII. OWASP A01:2021 Broken Access Control (IDOR).

Evidence

routes.php:713 `$router->get('/learn/enrol/success', 'CourseCatalogController@success');` is inside the public group opened at routes.php:340 (`$router->group('', [PerformanceMiddleware, ..., GateMiddleware], ...)`) — NO AuthMiddleware (verified: zero `});` between line 340 and 713). The handler in CourseCatalogController.php:245-310 does `$sessionId = trim($_GET['cs'] ?? '');` then `$purchase = TenantCoursePurchase::findBySession($sessionId);` (lookup by attacker-supplied session id, no user-ownership check — TenantCoursePurchase.php:23-30 selects purely `WHERE stripe_session_id = :sid`). It then renders `courses/success` which prints the buyer's email: templates/courses/success.php:40 `at <strong><?= $e($purchase['buyer_email'] ?? '') ?></strong>` and :57. There is also no `currentUser()` check anywhere in `success()`.

Suggested fix. In success(), require auth (or at minimum bind the purchase to the session user) and verify `(int)$purchase['buyer_user_id'] === (int)$currentUser['id']` (or match `$purchase['buyer_email']` to the authenticated user's email) before rendering or running enrolment. For guest checkouts, gate the page behind a one-time signed claim token tied to the purchase rather than the raw Stripe session id, and never render `buyer_email` to an unauthenticated viewer.

Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.


Patrick Bass
@mobieus

🚀 Jun 7, 2026 5:15am

Resolved — fixed and deployed. Commit e23a8fec596a, shipped dev-first then to all tenants on 2026-06-06.

In success(), added $this->requireAuth() then an ownership check: the viewer must be the buyer either via (int)$purchase['buyer_user_id'] === (int)$currentUser['id'] or a case-insensitive match of $purchase['buyer_email'] to the authenticated user's email. Non-buyers get a 404 + courses.success_access_denied security log, closing the IDOR where any known/guessed Stripe checkout session id exposed another buyer's purchase and could trigger their enrolment.

Status: fixed. Thread closed and locked.


Patrick Bass
@mobieus

Log in or register to reply to this thread.