From ac008d8a162e1ba449b62e5362712ded0eb52174 Mon Sep 17 00:00:00 2001 From: PanAchy Date: Tue, 19 May 2026 15:23:03 -0500 Subject: [PATCH] fix(console): verify payment ownership before generating receipt URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Billing.generateReceiptUrl was calling Stripe with a caller-supplied paymentID without checking that the payment belongs to the caller's workspace. Any authenticated user who knew a payment intent ID from another workspace could retrieve that workspace's Stripe receipt URL (billing email, card last-4, amount, date) — a classic IDOR. Fix: look up the payment in PaymentTable scoped to Actor.workspace() before calling Stripe. Throw if not found. --- packages/console/core/src/billing.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/console/core/src/billing.ts b/packages/console/core/src/billing.ts index f782968ed74a..7e4e745182e9 100644 --- a/packages/console/core/src/billing.ts +++ b/packages/console/core/src/billing.ts @@ -437,6 +437,21 @@ export namespace Billing { async (input) => { const { paymentID } = input + const payment = await Database.use((tx) => + tx + .select() + .from(PaymentTable) + .where( + and( + eq(PaymentTable.workspaceID, Actor.workspace()), + eq(PaymentTable.paymentID, paymentID), + isNull(PaymentTable.timeDeleted), + ), + ) + .then((rows) => rows[0]), + ) + if (!payment) throw new Error("Payment not found") + const intent = await Billing.stripe().paymentIntents.retrieve(paymentID) if (!intent.latest_charge) throw new Error("No charge found")