Supabase security · 6 minute read
Does Supabase CSRF apply to your app? Cookie vs localStorage sessions
A penetration test flags your Supabase-backed endpoint: dynamic Access-Control-Allow-Origin, Access-Control-Allow-Credentials: true, "CSRF vulnerability confirmed". Whether that finding is real or noise comes down to one question about your app: are Supabase requests authenticated by ambient cookies, or by an explicit bearer token? CSRF needs the former.
Why the CORS headers alone prove nothing
Access-Control-Allow-Credentials: true is a responseheader — it tells a browser that scripts on a listed origin may read credentialed responses. It does not mean the browser sent credentials, and it does not by itself enable a state-changing request. CORS governs reading responses cross-origin; CSRF is about the browser attaching credentials you never intended to attach. The headers can be perfectly correct while the CSRF claim is false — and a request rejected for "invalid compression format" demonstrates even less. You need the two questions below answered by your own app's behavior, not by headers.
Question 1: which client library authenticates your requests?
@supabase/supabase-js(default, localStorage sessions). The access token lives inlocalStorageand travels in anAuthorization: Bearerheader. A malicious page cannot read your localStorage (same-origin policy) and cannot make the browser attach your token. Classic CSRF against/rest/v1,/auth/v1and storage does not apply — the report is a hardening observation, not an exploitable flaw.@supabase/ssr(cookie-based sessions). The session lives in cookies, so the browser attaches them automatically. Now CSRF is a real threat surface: any state-changing endpoint that trusts only the cookie can be driven cross-site unless you add an origin or token check.
Migrating to the SSR helpers for Next.js App Router is the documented recommendation today — so more apps are moving onto the cookie side of this line without noticing the CSRF consequences. If you migrated, assume exposure until you have checked.
Question 2: what serves the flagged endpoint?
"/e/ on the project domain" needs a name before you remediate it:
- An Edge Function (typically
/functions/v1/e): check its JWT verification setting.verify_jwt = falsecombined with an identity the function derives from a cookie is exactly the combination that turns "dynamic CORS" into "attacker acts as the victim". With verification on, the function requires a token the attacker cannot mint. - Your own backend route: then standard CSRF defenses apply — tokens on forms,
SameSitecookies, and the origin check below.
The one check that closes both setups
Reject non-same-origin writes at the edge. Sec-Fetch-Site is set by the browser and cannot be forged by page JavaScript, so it is a reliable signal with no token plumbing:
// middleware.ts (Next.js) or your edge function
export function middleware(req: NextRequest) {
if (
req.method !== 'GET' &&
req.headers.get('sec-fetch-site') !== 'same-origin'
) {
return new Response('forbidden', { status: 403 })
}
return NextResponse.next()
}Pair it with an Origin allowlist for older clients, and you have covered localStorage apps (where it is belt-and-braces) and cookie apps (where it is the primary control) with the same code.
Self-check instead of waiting for the retest
- From an origin you control, send the same state-changing request with
credentials: 'include'and no bearer token — a benign one first (sign-out, a test insert). - It succeeds → real finding; fix the endpoint, not the CORS headers.
- It fails with 401 → the credentialed CORS headers are cosmetic for this endpoint. Report them separately as a CORS misconfiguration (origin reflection with credentials means any allowed origin's scripts can read authenticated responses) and move on.
The public RLS leak demo runs a read-only harness that queries as anon and as an authenticated user — the same role-replay method that tells you which identity your endpoint actually resolves for an unauthenticated caller.
Check every policy and role in one pass
Supabase RLS Audit Kit · $29 one-time
Seven commented SQL audits and 60 checks show every table's RLS status, the role each policy actually targets, grants and bypass paths, plus a role-simulation harness that proves isolation. Report and fix-SQL templates included — useful when a pentest sends you back to "prove the endpoint's effective role".
Get the kit - $29 ↗Review the files, scope and license →Related reading: 7 ways your Supabase app still leaks data with RLS enabled and what your AI security reviewer structurally cannot see. For a full launch pass, a fixed-price Supabase RLS audit from $99 returns prioritized findings with reproducible evidence.