Least privilege, every request

Give each role only what it needs; check access on every request.

OWASPowasp.org
OWASP Top 10
ReferenceRead docs ↗

From security-best-practices: OWASP A01 Broken Access Control.

Change the ID in the URL

Signed in as Ava at Northwind. Try another invoice ID.

Invoice IDs 1040 to 1047 exist.

Signed in is enoughBad

.eq("id", id)
Ownership in the queryGood

.eq("id", id).eq("company_id", user.companyId)
export async function GET(req, { params }) {
  const user = await getAuthenticatedUser();
  if (!user) return new Response(null, { status: 401 });

  const { data, error } = await db.from("invoices")
    .select("*")
    .eq("id", params.id)
    .eq("company_id", user.companyId) // ownership lives in the query
    .maybeSingle();

  if (error) return new Response(null, { status: 500 });
  // 404, not 403: don't confirm that someone else's ID exists
  if (!data) return new Response(null, { status: 404 });
  return Response.json(data);
}

Edit roles, then call the API

Tick permissions, pick a role, and Send hidden actions.

Permissions per role
PermissionAdminMemberViewer
Hidden in the UI onlyBad

    {can(role, a) && <Button />}
    Checked on the serverGood

      authorize(user, action) // 403

      Switch off one layer

      Toggle layers and watch which attacks reach the data.

      Request path
        app.use(requireHttps);                                      // HSTS, Secure cookies
        app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 })); // 429
        app.use(authenticate);                                      // 401
        app.use(validate(schema));                                  // Zod, unknown keys stripped
        app.use(authorize);                                         // role + ownership, 403
        
        -- in the database
        create policy tenant_isolation on invoices
          using (company_id in (
            select company_id from company_users where user_id = auth.uid()
          ));

        Why it matters, pros and cons

        Why it matters

        Anything the client sends can be forged, so the server decides every time.

        Pros

        • A stolen viewer session can only read.
        • Guessed IDs return nothing.
        • One missed check is caught by the next layer.

        Cons

        • Every route needs the check, and a test.
        • Too-fine permissions turn into support tickets.
        • RLS and app checks can drift apart.