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,7 +1,7 @@
import { FastifyInstance } from 'fastify';
import { BillingService } from '@shieldai/shared-billing/src/services/billing.service';
import { SubscriptionService, customerService, webhookService } from '@shieldai/shared-billing/src/services/billing.services';
import { SubscriptionTier } from '@shieldai/shared-billing/src/config/billing.config';
import { SubscriptionTier, isValidReturnUrl } from '@shieldai/shared-billing/src/config/billing.config';
import { AuthRequest } from './auth.middleware';
const billingService = BillingService.getInstance();
@@ -218,6 +218,13 @@ export async function subscriptionRoutes(fastify: FastifyInstance) {
});
}
if (!isValidReturnUrl(returnUrl)) {
return reply.status(400).send({
error: 'Invalid return URL',
message: 'returnUrl must be from an allowed origin',
});
}
try {
const portalSession = await billingService.createCustomerPortalSession(
customerId,