Deny by default in one server-side helper
Put session verification in a server-only module and call it at the top of every route handler, Server Action and tool handler. Here auth() stands for your auth library's session call. In code review, treat any handler that does not call the helper as a bug.
import 'server-only'
export async function requireUser() {
const session = await auth()
if (!session?.user) throw new Error('Unauthorized')
return session.user
}
Check existence and ownership on the server for every object
Load the record and return 404 if it is missing. Before any read or write, compare its owner with the session user. Never trust an owner id, role or isAdmin flag sent by the client.
const user = await requireUser()
const review = await db.review.findUnique({ where: { id } })
if (!review) return new Response(null, { status: 404 })
if (review.authorId !== user.id) return new Response(null, { status: 403 })
Put authentication in front of tool servers
Keep an internal MCP or admin server off the public internet, or require a bearer token on every request and answer a missing or invalid token with 401. The MCP specification says HTTP servers that support authorization must check that the token was issued for them and must never pass it on to upstream APIs. Remove any tool that returns environment variables.
Keep secrets out of client code and scan the build
Never give a secret a NEXT_PUBLIC_ or VITE_ prefix. Add import 'server-only' to every module that reads process.env, so importing it from the client fails the build. Before each release, search the built client output for secret names and key prefixes.
npm run build
grep -rlE 'sk_live_|sb_secret_|SERVICE_ROLE|SECRET_KEY' .next/static dist 2>/dev/null
Rotate anything that shipped
Removing a key from the code does not remove it from builds that are already deployed or cached. Issue a new key at the provider, deploy it as a server-only variable, then revoke the old one.
Build outbound URLs from an allowlist
Accept a short name, map it on the server to a fixed origin, and reject anything else. Do not accept full URLs from the request. Set redirect: 'error' so a redirect cannot send the request to another origin.
const ORIGINS = { images: 'https://images.example.com' } as const
const origin = ORIGINS[target as keyof typeof ORIGINS]
if (!origin) return new Response(null, { status: 400 })
const res = await fetch(new URL('/v1/render', origin), {
redirect: 'error',
headers: { Authorization: `Bearer ${process.env.RENDER_TOKEN}` },
})
Make demo accounts read-only, or reset them
Enforce read-only access for the demo user on the server, not by hiding buttons. If visitors must be able to write, restore the seed data on a schedule. Restore it now as well, since the live data may already have been changed.