Forums Bug Reports Thread

Paid-forum access is granted at checkout START, not on payment completion — free bypass of paid communities

Patrick Bass · Jun 6 · 10 · 1 Locked
[Critical] [Urgent] [Bug Fixed] [Always Reproduces]
🚀 OP Jun 6, 2026 5:27pm

Area: Forums (audit p2) · Surface: POST /forums/{slug}/subscribe (ForumController@subscribeForum) · Dimension: Broken Access Control / Business Logic (OWASP A01:2021) · Severity: critical

A logged-in user can obtain permanent, free access to any paid forum by simply POSTing to /forums/{slug}/subscribe and abandoning the Stripe checkout page (or just letting the redirect 302 fire and closing the tab). The 'active' subscription row is created before the user ever pays, and because no Stripe webhook handles the 'forum_subscription' metadata type, that row is never reverted to 'pending'/'past_due'/expired. Forum::hasPaidAccess() — which gates canView()/canPost()/passesAccessRule('paid') for the whole paid-forum feature — returns true for any 'active' row, so the attacker reads and posts in the paid forum indefinitely without payment. This applies to both recurring ('monthly'/'yearly') and one-time ('once') price intervals.

Evidence

platform/src/Controllers/ForumController.php:1113-1118 — immediately after StripeCheckout::createSession() and BEFORE redirecting the user to Stripe, the controller writes an ACTIVE subscription row:
  $this->db->execute(
    "INSERT INTO forum_subscriptions (forum_id, user_id, stripe_subscription_id, status)
     VALUES (:fid, :uid, :sid, 'active')
     ON DUPLICATE KEY UPDATE status = 'active', ...",
    ['fid'=>$forum['id'],'uid'=>$user['id'],'sid'=>$result['session_id'] ?? '']);
The redirect to the Stripe checkout page happens AFTER this insert (line 1120 $this->redirect($result['url'])). Access is then granted on any 'active' row: platform/src/Models/Forum.php:445-449 hasPaidAccess() — "SELECT id FROM forum_subscriptions WHERE forum_id=:fid AND user_id=:uid AND status='active'" → returns true. The paid table defaults status to 'active' (platform/database/migrations/2026-05-26-paid-forums.sql:14 `status enum(...) NOT NULL DEFAULT 'active'`). grep confirms NO webhook ever reconciles forum subscriptions: the only writes to this table's status column are the pre-payment INSERT (ForumController.php:1114) and the cancel UPDATE (ForumController.php:1134); TenantStripeController.php has zero references to forum_subscription/forum_id (grep returned empty).

Suggested fix. Do not write an 'active' subscription on checkout creation. Either (a) insert with a non-granting status (e.g. add a 'pending' enum value and INSERT status='pending'), and flip to 'active' only from a verified Stripe webhook (checkout.session.completed / invoice.paid) keyed on the session/subscription id; or (b) remove the pre-payment INSERT entirely and have the webhook upsert the row on payment success. Add a handler in TenantStripeController for metadata.mobieus_type='forum_subscription' that validates the event signature, then upserts (forum_id,user_id,status='active',current_period_end). Ensure hasPaidAccess() also checks current_period_end for recurring plans.

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


Patrick Bass
@mobieus

🚀 Jun 7, 2026 10:12am

Resolved and deployed to production. Commit 727f6d6555.

Root cause: the 2026-05-26 paid-forum migration used CREATE TABLE IF NOT EXISTS forum_subscriptions, which silently no-oped because that table name was already taken by the notification-subscription system. Paid-forum INSERTs were failing, so the feature was broken but not exploitable (access was never granted, contrary to the original finding).

Full fix: (1) New table forum_paid_subscriptions with a status=pending default and current_period_end. (2) Forum::hasPaidAccess() now queries the new table and checks current_period_end for recurring plans. (3) subscribeForum() inserts with status=pending (not active) before the Stripe redirect — closing the pre-payment access-grant. (4) TenantStripeController now intercepts checkout.session.completed events with metadata.mobieus_type=forum_subscription and flips the row to active with the session/subscription id and period end. Migration applied to all tenants.

Status: fixed. Thread closed.


Patrick Bass
@mobieus

Log in or register to reply to this thread.