Cenk Kurtoğlu©26

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?

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:

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

  1. 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).
  2. It succeeds → real finding; fix the endpoint, not the CORS headers.
  3. 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.

Template bundle

Review all 20 frontend starters in one bundle

20 editable frontend starters with live demos are $79 — one payment, no subscription.

View the Bundle — $79↗