Articles · Security and vulnerabilities
Authentication Traps Under the Hood of AI-Generated Web Apps
Sign-in screens look finished long before authentication is safe. We break down cookie-presence checks, client-supplied user IDs, unverified webhooks, and token leakage.
The four traps, and the fix for each
- A cookie exists, so the user is signed inVerify the session on every read and write
- The browser says which user it isTake the user from the verified session
- Webhooks are unsigned or replayedCheck the raw-body signature; record event ids
- A service key gets a NEXT_PUBLIC_ nameKeep privileged keys on the server
On this page
When you ask an AI coding assistant to add sign-in to a web application, the visual result arrives in minutes: a polished login form, a redirect to /dashboard, and a sign-out button in the header.
Underneath that UI, however, AI assistants optimize for making the screen work on the first run. Unless prompted with strict security constraints, they frequently take shortcuts that leave user data exposed.
Here are four authentication and authorization traps we look for first when reviewing a codebase.
Trap 1: Checking cookie presence instead of verifying the session
Routing layers often check whether a session cookie exists to decide whether to redirect a visitor to /login. That check is fine as a fast UI redirect, provided the data layer still verifies the session token on the server.
The failure happens when route handlers or server components assume that reaching the page means the request is authenticated:
// Broken check: only tests whether a cookie string is presentconst token = cookieStore.get("session_token")?.value;if (!token) { redirect("/login");}// Danger: never verified signature, expiration, or revocationA routing redirect is a navigation convenience, not an access control boundary. Every data read and mutation must verify the session signature and expiration server-side.
- 1 · Browser requestUntrusted caller input
cookie: session_token · body: { userId } - 2 · Route redirectNavigation convenience only
if (!token) redirect("/login") - 3 · Server data layerReal authorization boundary
verifySession() + where: { id, orgId }
Trap 2: Trusting userId or organizationId from the request body
When generating a form or API call, AI tools often pass userId or orgId as a hidden field or JSON property from the client component to the server handler.
// Broken pattern: trusts the caller to state who they areexport async function getBillingHistory(requestedUserId: string) { return db.invoice.findMany({ where: { userId: requestedUserId }, });}An attacker does not need to break your cryptography if they can pass another customer's UUID into getBillingHistory. Derive the actor's identity exclusively from the verified server session, and verify membership before querying organization data.
Trap 3: Unsigned or non-idempotent auth and billing webhooks
Modern apps rely on webhooks from authentication providers (such as Clerk, Auth0, or Supabase) and payment processors (such as Stripe or Paddle) to provision accounts and upgrade plans.
Two defects are common in generated webhook handlers:
- Skipping signature verification (or reading the parsed JSON body instead of the raw request bytes before calling
constructEvent), allowing anyone who discovers the endpoint URL to forge acheckout.session.completedevent. - Missing event-ID deduplication, causing duplicate account provisioning or double credits when the provider retries a slow
200response.
// Always verify the raw payload signature before parsing event dataconst rawBody = await request.text();const signature = request.headers.get("stripe-signature");if (!signature) { return new Response("Missing signature", { status: 400 });}const event = stripe.webhooks.constructEvent(rawBody, signature, webhookSecret);Trap 4: Leaking privileged keys into the client bundle
In Next.js and similar frameworks, any environment variable prefixed with NEXT_PUBLIC_ is inlined into the browser JavaScript bundle at build time.
When an AI assistant hits a missing environment variable error in a client component, a common automated "fix" is to rename SUPABASE_SERVICE_ROLE_KEY or an internal API key to NEXT_PUBLIC_... so the error disappears. That single rename hands full database bypass privileges to anyone who opens browser DevTools.
SUPABASE_SERVICE_ROLE_KEYKept behind import "server-only"; never sent to visitors.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEYInlined into public /_next/static/*.js; visible in DevTools.// lib/db-admin.ts — isolate privileged clients to the serverimport "server-only";import { createClient } from "@supabase/supabase-js";export const adminDb = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!, process.env.SUPABASE_SERVICE_ROLE_KEY!,);How to verify your own repository
- Search the codebase for every
NEXT_PUBLIC_variable and confirm none hold service-role keys, webhook secrets, or private API tokens. - List every Server Action and
POST/PUT/DELETEroute handler, and trace where the actinguserIdcomes from. - Confirm every multi-tenant query filters by both the resource ID and the verified tenant or owner ID.
If you want a second pair of engineering eyes on a live or launching application, see how a Codeneath Review works or get in touch.
A Codeneath Review reads your repository line by line. From $499, in about 5 working days.
See how a Review works →Want this check on your codebase?
A Codeneath Review from $499: six answers at file:line, fix prompts and a developer hand-off sheet.