DEV Community

Cover image for Stop the Supabase getSession() Security Warning
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on Originally published at iloveblogs.blog

Stop the Supabase getSession() Security Warning

What actually changed

You upgraded @supabase/auth-js to v2 (or pulled @supabase/ssr into your Next.js App Router project), and now your terminal fills up with this on every protected route:

Using the user object as returned from supabase.auth.getSession() or
from some supabase.auth.onAuthStateChange() events could be insecure if the
user object is used for authorization purposes. This is because the user
object is not validated on the server and can be tampered with.
Use supabase.auth.getUser() instead.
Enter fullscreen mode Exit fullscreen mode

It is not a thrown error — your app keeps working — so most teams ignore it for weeks. Then a security review flags it, or a forged-cookie attack vector shows up in a pentest, and the warning becomes a P0. The issue has been the most-commented auth-js issue for over a year (supabase/auth-js#873).

The fix

The warning is telling you exactly what to do, but not where. The rule:

  • Use getUser() for any decision that grants or denies access. Middleware, Server Component gating, Server Actions, Route Handlers.
  • Use getSession() only for optimistic client UI — rendering a username before the server has confirmed the session.

Before — the code that triggers the warning

// src/middleware.ts (or any Server Component)
import { createServerClient } from '@supabase/ssr'

const supabase = createServerClient(url, anonKey, { cookies })

// ❌ Reads the cookie, does NOT validate the JWT with the Auth server.
const { data: { session } } = await supabase.auth.getSession()

if (!session) {
  return NextResponse.redirect(loginUrl) // gated on unvalidated data
}
Enter fullscreen mode Exit fullscreen mode

getSession() returns whatever is in the cookie. If an attacker crafts a cookie with a fake JWT, getSession() happily hands back a session object and your redirect never fires — or worse, your protected data leaks.

After — validate server-side

// src/middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => request.cookies.getAll(),
        setAll: (toSet) =>
          toSet.forEach(({ name, value, options }) =>
            request.cookies.set(name, value),
          ),
      },
    },
  )

  // ✅ Validates the JWT against the Supabase Auth server.
  const { data: { user } } = await supabase.auth.getUser()

  if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}
Enter fullscreen mode Exit fullscreen mode

The same swap applies to Server Components and Server Actions — replace getSession() with getUser() and destructure user, not session. The user object is what you gate on; the session is for the client.

Where getSession() is still correct

'use client'

import { createClient } from '@/lib/supabase/client'

export function HeaderAvatar() {
  const supabase = createClient()
  const [email, setEmail] = useState<string | null>(null)

  useEffect(() => {
    // ✅ Optimistic UI — fine. Not used for authorization.
    supabase.auth.getSession().then(({ data: { session } }) => {
      setEmail(session?.user?.email ?? null)
    })
  }, [])

  return <span>{email ?? 'Not signed in'}</span>
}
Enter fullscreen mode Exit fullscreen mode

This is the only place getSession() belongs. If a fake cookie fools it, the worst outcome is a wrong email shown for a second until the real session loads. No data is exposed.

Verifying the fix

  1. grep -rn "auth.getSession" src/ — every hit outside a 'use client' file must change to getUser().
  2. Run your dev server and load a protected route. The warning should stop printing.
  3. Forge a cookie manually (set sb-access-token to a random string in DevTools → Application → Cookies) and reload. With getUser(), the route redirects to /login. With the old getSession() code, it would have let you through.

The deeper background on why this distinction matters for the entire request lifecycle — middleware refresh, Server Component reads, Client Component sync — is in the Next.js + Supabase SSR session management deep dive, which walks through the cookie refresh chain that makes getUser() safe without doubling your latency.

Related Incidents


Originally published at https://www.iloveblogs.blog

Top comments (0)