From 0af514ce5ce33bd0724e05fe1ecad5a5658a5636 Mon Sep 17 00:00:00 2001 From: rissrice2105-agent <289161642+rissrice2105-agent@users.noreply.github.com> Date: Sun, 5 Jul 2026 23:13:29 -0600 Subject: [PATCH] fix: restrict auth callback redirects --- apps/web/src/app/auth/callback/route.test.ts | 20 ++++++++++++++++++++ apps/web/src/app/auth/callback/route.ts | 10 +++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/app/auth/callback/route.test.ts diff --git a/apps/web/src/app/auth/callback/route.test.ts b/apps/web/src/app/auth/callback/route.test.ts new file mode 100644 index 0000000..1bc0af0 --- /dev/null +++ b/apps/web/src/app/auth/callback/route.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; + +import { safeNextPath } from './route'; + +describe('safeNextPath', () => { + it('keeps internal callback paths', () => { + expect(safeNextPath('/dashboard')).toBe('/dashboard'); + expect(safeNextPath('/session/abc?tab=chat')).toBe('/session/abc?tab=chat'); + }); + + it('falls back for external callback targets', () => { + expect(safeNextPath('https://evil.example/phish')).toBe('/dashboard'); + expect(safeNextPath('//evil.example/phish')).toBe('/dashboard'); + }); + + it('falls back for missing or relative callback targets', () => { + expect(safeNextPath(null)).toBe('/dashboard'); + expect(safeNextPath('dashboard')).toBe('/dashboard'); + }); +}); diff --git a/apps/web/src/app/auth/callback/route.ts b/apps/web/src/app/auth/callback/route.ts index 41743c0..f247c00 100644 --- a/apps/web/src/app/auth/callback/route.ts +++ b/apps/web/src/app/auth/callback/route.ts @@ -2,12 +2,20 @@ import { createServerClient, type CookieOptions } from '@supabase/ssr'; import { NextResponse, type NextRequest } from 'next/server'; +export function safeNextPath(next: string | null): string { + if (!next || !next.startsWith('/') || next.startsWith('//')) { + return '/dashboard'; + } + + return next; +} + export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const code = searchParams.get('code'); const tokenHash = searchParams.get('token_hash'); const type = searchParams.get('type'); - const next = searchParams.get('next') ?? '/dashboard'; + const next = safeNextPath(searchParams.get('next')); const origin = process.env.NEXT_PUBLIC_APP_URL ?? 'https://pairux.com'; const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL ?? ''; const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? '';