From b30f1740fc40e460948d3e0c806649100808d2b8 Mon Sep 17 00:00:00 2001 From: FuturMix Date: Sun, 14 Jun 2026 12:06:32 +0800 Subject: [PATCH] fix: prevent open redirect via returnTo parameter in auth flow The returnTo parameter from the query string is stored in a cookie and used as the redirect target after OAuth callback. An attacker can set returnTo to an absolute URL (e.g. https://evil.com) to redirect users to a malicious site after login. Validate returnTo in both the auth initiation and callback routes to ensure it starts with / and is not a protocol-relative URL (//). Co-Authored-By: Claude Opus 4.6 --- apps/web/app/api/auth/callback/route.ts | 6 +++++- apps/web/app/api/auth/coinpay/route.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/web/app/api/auth/callback/route.ts b/apps/web/app/api/auth/callback/route.ts index c1f8149..7b122e3 100644 --- a/apps/web/app/api/auth/callback/route.ts +++ b/apps/web/app/api/auth/callback/route.ts @@ -14,7 +14,11 @@ export async function GET(req: NextRequest) { const storedState = req.cookies.get('cp_state')?.value; const codeVerifier = req.cookies.get('cp_pkce')?.value; - const returnTo = req.cookies.get('cp_return')?.value ?? '/'; + let returnTo = req.cookies.get('cp_return')?.value ?? '/'; + // Prevent open redirect — only allow relative paths + if (!returnTo.startsWith('/') || returnTo.startsWith('//')) { + returnTo = '/'; + } const fail = (msg: string) => NextResponse.redirect(new URL(`/?auth_error=${encodeURIComponent(msg)}`, APP_URL)); diff --git a/apps/web/app/api/auth/coinpay/route.ts b/apps/web/app/api/auth/coinpay/route.ts index ab17cf7..2fac34c 100644 --- a/apps/web/app/api/auth/coinpay/route.ts +++ b/apps/web/app/api/auth/coinpay/route.ts @@ -9,7 +9,11 @@ function base64url(buf: ArrayBuffer | Uint8Array): string { } export async function GET(req: NextRequest) { - const returnTo = req.nextUrl.searchParams.get('returnTo') ?? '/'; + let returnTo = req.nextUrl.searchParams.get('returnTo') ?? '/'; + // Prevent open redirect — only allow relative paths + if (!returnTo.startsWith('/') || returnTo.startsWith('//')) { + returnTo = '/'; + } // PKCE const verifierBytes = crypto.getRandomValues(new Uint8Array(32));