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:
@@ -52,6 +52,32 @@ export const BillingConfigSchema = z.object({
|
||||
|
||||
export type BillingConfig = z.infer<typeof BillingConfigSchema>;
|
||||
|
||||
// Allowed return URL origins for Stripe customer portal (open redirect prevention)
|
||||
const allowedReturnUrlOrigins: string[] = (process.env.ALLOWED_RETURN_URL_ORIGINS || 'https://app.shieldai.com,shieldai://').split(',').map(s => s.trim()).filter(Boolean);
|
||||
|
||||
export function isValidReturnUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
// Custom app schemes (e.g., shieldai://) have origin === 'null'
|
||||
// Use prefix matching for these since they don't have a standard origin
|
||||
if (parsed.origin === 'null') {
|
||||
return allowedReturnUrlOrigins.some(origin => url.startsWith(origin));
|
||||
}
|
||||
// Standard web protocols: compare origins to prevent substring attacks
|
||||
return allowedReturnUrlOrigins.some(origin => {
|
||||
try {
|
||||
const originUrl = new URL(origin);
|
||||
return parsed.origin === originUrl.origin;
|
||||
} catch {
|
||||
// If the allowed origin can't be parsed as a URL, it's a custom scheme
|
||||
return false;
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const loadBillingConfig = (): BillingConfig => {
|
||||
const rawConfig = {
|
||||
stripe: {
|
||||
|
||||
Reference in New Issue
Block a user