From ecbfa533295a7eaf44763cc4e1d42277da70d410 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 14 Oct 2026 17:11:12 +0000 Subject: [PATCH 1/3] Verify session tokens before trusting server-side identity getSession() returned the session straight from the auth cookie, and supabase.auth.getSession() does not check the access token signature, so server-side owner and admin checks trusted unverified cookie contents. Derive the user from getClaims() instead, which verifies the JWT against the project's JWKS (or the Auth server for symmetric keys). Co-authored-by: Eric Zakariasson --- apps/cursor/src/utils/supabase/auth.ts | 38 ++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/apps/cursor/src/utils/supabase/auth.ts b/apps/cursor/src/utils/supabase/auth.ts index 77f4b304..b5fe27c0 100644 --- a/apps/cursor/src/utils/supabase/auth.ts +++ b/apps/cursor/src/utils/supabase/auth.ts @@ -1,9 +1,37 @@ import { createClient } from "./server"; -export async function getSession() { +export type VerifiedSession = { + user: { + id: string; + email: string | undefined; + user_metadata: Record; + }; +}; + +/** + * The signed-in user, read from the access token's verified claims. + * + * `supabase.auth.getSession()` returns whatever the session cookie contains + * without checking the token signature, so its `user` must never back an + * authorization decision on the server. `getClaims()` verifies the JWT + * (against the project's JWKS, or via the Auth server for symmetric keys) + * before we trust `sub`. + */ +export async function getSession(): Promise { const supabase = await createClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - return session; + const { data, error } = await supabase.auth.getClaims(); + + if (error || !data?.claims.sub) { + return null; + } + + const { claims } = data; + + return { + user: { + id: claims.sub, + email: claims.email, + user_metadata: claims.user_metadata ?? {}, + }, + }; } From 645277ae6decbee6cf3f1bd6bb2c28ec1d8ef515 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 14 Oct 2026 17:11:12 +0000 Subject: [PATCH 2/3] Keep auth callback redirects on the same origin The callback appended the raw next query param to the base URL, so values such as @evil.example redirected users off-site after signing in. Resolve next against the request origin and fall back to / for anything that leaves it. Co-authored-by: Eric Zakariasson --- apps/cursor/src/app/auth/callback/route.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/cursor/src/app/auth/callback/route.ts b/apps/cursor/src/app/auth/callback/route.ts index c820a3c9..207d72c5 100644 --- a/apps/cursor/src/app/auth/callback/route.ts +++ b/apps/cursor/src/app/auth/callback/route.ts @@ -1,10 +1,23 @@ import { NextResponse } from "next/server"; import { createClient } from "@/utils/supabase/server"; +// `next` comes from the query string and is appended to a base URL, so values +// like `@evil.example` or `//evil.example` would otherwise leave the site. +function safeNextPath(next: string | null, origin: string): string { + if (!next) return "/"; + try { + const url = new URL(next, origin); + if (url.origin !== origin) return "/"; + return `${url.pathname}${url.search}${url.hash}`; + } catch { + return "/"; + } +} + export async function GET(request: Request) { const { searchParams, origin } = new URL(request.url); const code = searchParams.get("code"); - const next = searchParams.get("next") ?? "/"; + const next = safeNextPath(searchParams.get("next"), origin); if (code) { const supabase = await createClient(); From e437a94f698cb6ef5a29ffafe9cd223ac940e3b8 Mon Sep 17 00:00:00 2001 From: ericzakariasson Date: Wed, 14 Oct 2026 17:36:47 -0700 Subject: [PATCH 3/3] Treat an expired access token as signed out getClaims() in @supabase/auth-js 2.105 throws a plain Error for an expired token instead of returning it as an error, so getSession() let the exception escape and server pages that call it could 500. Any getClaims failure now means there's no verified session. Co-authored-by: Cursor --- apps/cursor/src/utils/supabase/auth.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/cursor/src/utils/supabase/auth.ts b/apps/cursor/src/utils/supabase/auth.ts index b5fe27c0..4c933fa1 100644 --- a/apps/cursor/src/utils/supabase/auth.ts +++ b/apps/cursor/src/utils/supabase/auth.ts @@ -19,13 +19,14 @@ export type VerifiedSession = { */ export async function getSession(): Promise { const supabase = await createClient(); - const { data, error } = await supabase.auth.getClaims(); + // getClaims throws, rather than returning an error, for an expired token. + const result = await supabase.auth.getClaims().catch(() => null); - if (error || !data?.claims.sub) { + if (!result || result.error || !result.data?.claims.sub) { return null; } - const { claims } = data; + const { claims } = result.data; return { user: {