ShipSafeFull checklist (23 checks)

Auth and database

Check permissions on the server

Check the session and record ownership in every API route and server action. Hidden buttons are not security. Includes CORS with credentials.

Why it matters

Hiding a button does nothing; anyone can call your API with curl. The common bug is an insecure direct object reference: the route loads a record by the ID it was sent without checking it belongs to the caller.

How to fix it

  1. In every API route and server action, get the user from the session, not from the request body.
  2. Load the record and confirm the user owns it or has a role that allows the action.
  3. Return 404 or 403 when they don't.
  4. If the API uses cookies, don't combine Access-Control-Allow-Origin: * with credentials; list your exact origins.
export async function DELETE(req: Request, { params }: { params: { id: string } }) {
  const user = await getSessionUser(); // from the cookie, not the body
  if (!user) return new Response(null, { status: 401 });
  const doc = await db.doc.findUnique({ where: { id: params.id } });
  if (!doc || doc.ownerId !== user.id) return new Response(null, { status: 404 });
  await db.doc.delete({ where: { id: doc.id } });
  return new Response(null, { status: 204 });
}

How to verify

Questions

Is middleware enough?
Middleware can check that a user is signed in. Ownership of a specific record still has to be checked where the record is loaded.

Related checks

Tick it off in the full checklist

Get new checks by email

New checks and real leak post-mortems, about once a month. No spam.