Fix open redirect in Stripe customer portal returnUrl (FRE-5399)

- Add isValidReturnUrl validation at route level for fast rejection
- Add defense-in-depth validation in BillingService.createCustomerPortalSession
- Fix isValidReturnUrl bug: origin comparison was never reached due to
  incorrect protocol check, allowing substring attacks (e.g., app.shieldai.com.evil.com)
- Export isValidReturnUrl from shared-billing package index
- Add unit tests for all attack vectors

Files changed:
- packages/api/src/routes/subscription.routes.ts
- packages/shared-billing/src/services/billing.service.ts
- packages/shared-billing/src/config/billing.config.ts
- packages/shared-billing/src/index.ts
- packages/shared-billing/src/__tests__/billing.config.test.ts
This commit is contained in:
Founding Engineer
2026-05-17 05:39:13 -04:00
committed by Michael Freno
parent e72a0ba5cf
commit 7fb8b83810
5 changed files with 98 additions and 3 deletions

View File

@@ -1,5 +1,5 @@
import Stripe from 'stripe';
import { loadBillingConfig, SubscriptionTier } from '../config/billing.config';
import { loadBillingConfig, SubscriptionTier, isValidReturnUrl } from '../config/billing.config';
import { RedisService } from '@shieldsai/shared-notifications';
import type { Subscription, SubscriptionCreateSchema, SubscriptionUpdateSchema } from '../models/subscription.model';
@@ -168,6 +168,9 @@ export class BillingService {
customerId: string,
returnUrl: string
): Promise<Stripe.BillingPortal.Session> {
if (!isValidReturnUrl(returnUrl)) {
throw new Error(`Invalid return URL: ${returnUrl}`);
}
return await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: returnUrl,