an app to share curated trails sidetrail.app
atproto nextjs react rsc

Fix redirect

+24 -2
+13 -1
app/oauth/callback/route.ts
··· 11 11 return `${proto}://${host}`; 12 12 } 13 13 14 + function isSameOrigin(url: string, baseUrl: string): boolean { 15 + try { 16 + return new URL(url, baseUrl).origin === new URL(baseUrl).origin; 17 + } catch { 18 + return false; 19 + } 20 + } 21 + 14 22 async function handleCallback(request: NextRequest) { 15 23 const baseUrl = getBaseUrl(request); 16 24 const params = new URLSearchParams(request.nextUrl.search); ··· 35 43 if (state) { 36 44 try { 37 45 const parsed = JSON.parse(state); 38 - if (parsed.returnUrl && typeof parsed.returnUrl === "string" && parsed.returnUrl.startsWith('/')) { 46 + if ( 47 + parsed.returnUrl && 48 + typeof parsed.returnUrl === "string" && 49 + isSameOrigin(parsed.returnUrl, baseUrl) 50 + ) { 39 51 returnUrl = parsed.returnUrl; 40 52 } 41 53 } catch {
+11 -1
auth/actions.ts
··· 4 4 import { redirect } from "next/navigation"; 5 5 import { getOAuthClient, getSession } from "./index"; 6 6 7 + function isSameOrigin(url: string): boolean { 8 + try { 9 + // Use a dummy base - we only care that the URL doesn't escape to a different origin 10 + const base = "https://self"; 11 + return new URL(url, base).origin === base; 12 + } catch { 13 + return false; 14 + } 15 + } 16 + 7 17 export async function login(formData: FormData) { 8 18 const rawHandle = formData.get("loginHint") as string; 9 19 let returnUrl = (formData.get("returnUrl") as string) || "/"; 10 - if (!returnUrl.startsWith("/")) { 20 + if (!isSameOrigin(returnUrl)) { 11 21 returnUrl = "/"; 12 22 } 13 23