Skip to content

fix(console): verify payment ownership before generating receipt URL#28396

Open
PanAchy wants to merge 5 commits into
anomalyco:devfrom
PanAchy:fix/billing-receipt-url-idor
Open

fix(console): verify payment ownership before generating receipt URL#28396
PanAchy wants to merge 5 commits into
anomalyco:devfrom
PanAchy:fix/billing-receipt-url-idor

Conversation

@PanAchy
Copy link
Copy Markdown
Contributor

@PanAchy PanAchy commented May 19, 2026

Issue for this PR

Closes #28395

Type of change

  • Bug fix

What does this PR do?

Billing.generateReceiptUrl was passing the caller-supplied paymentID directly to Stripe without first checking that the payment belongs to the caller's workspace. This is an IDOR (Insecure Direct Object Reference): any authenticated OpenCode user who knows a Stripe payment intent ID from another workspace could retrieve that workspace's Stripe receipt URL, which contains their billing email, card last-4, amount, and transaction date.

The fix adds an ownership check before calling Stripe. The function now looks up the payment in PaymentTable filtered by both paymentID and workspaceID = Actor.workspace(). If no matching record is found, it throws before making any Stripe API call.

// Before (no ownership check):
const intent = await Billing.stripe().paymentIntents.retrieve(paymentID)

// After (ownership verified first):
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)

No schema changes. No migrations needed. The fix is contained to a single function in packages/console/core/src/billing.ts.

How did you verify your code works?

  • Ran bun typecheck in packages/console/core — no errors.
  • Reviewed the PaymentTable schema to confirm workspaceID and paymentID columns exist and timeDeleted is part of workspaceColumns.
  • Traced the call site (payment-section.tsx:26) to confirm the function runs inside an authenticated actor context, so Actor.workspace() is always available when this function is called.
  • A valid payment for the caller's workspace still resolves correctly. An unknown or cross-workspace payment ID throws before any Stripe call is made.
  • No automated test added: the existing console test suite only covers pure functions (subscription.test.ts, date.test.ts, rateLimiter.test.ts) with no mocking infrastructure. generateReceiptUrl requires a live DB connection, Stripe API, and SST resource context — testing it purely is not feasible within the current test conventions. The fix is verifiable by code review: the DB lookup is scoped to Actor.workspace() before any Stripe call is made.

Screenshots / recordings

If this is a UI change, please include a screenshot or recording.

Checklist

  • I have tested my changes locally (checking for PR guideline, but can't actually test it due to statement above on automated test addition)
  • I have not included unrelated changes in this PR

If you do not follow this template your PR will be automatically rejected.

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.
@github-actions github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. contributor and removed needs:compliance This means the issue will auto-close after 2 hours. labels May 19, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IDOR in generateReceiptUrl

1 participant