The browser is not a security boundary
A user can call an API directly even when the interface hides a button. Every protected request must independently establish identity, tenant context, permission and resource scope on the server.
Protect lists and individual records
List routes must filter data before it leaves the database. Detail, update and delete routes must retrieve the object through the same authorized scope. Checking permission after a global lookup can reveal whether another tenant’s record exists.
- Default to no access when context is missing
- Scope queries by organization or ownership
- Check the action required for the route
- Conceal resource existence when appropriate
- Audit sensitive decisions without logging secrets
Prevent identifier bypass
Sequential numbers, UUIDs and signed links can all be copied or guessed. A strong identifier helps, but it never replaces authorization. Tests should deliberately use another tenant’s identifiers against list, detail, update and delete routes.
Make enforcement reusable
Shared permission classes, scoped-query helpers and policy tests reduce inconsistent inline checks. The goal is a small number of verified enforcement paths that every protected API is required to use.