Section 1: API security foundations
Object level authorisation in practice
Broken object level authorisation (BOLA) is the most common serious API flaw: a caller can access an object they should not, simply by referring to its identifier.
The mistake
app.get("/projects/:projectId/tasks/:taskId", requireAuth, async (req, res) => {
const task = await db.task.findUnique({ where: { id: req.params.taskId } });
if (!task) return res.sendStatus(404);
res.json(task);
});
The caller is authenticated, but nothing checks that the task belongs to a project in the caller's organisation, or even that it belongs to the project in the path.
The fix: resolve access through the caller
app.get("/projects/:projectId/tasks/:taskId", requireAuth, async (req, res) => {
const task = await db.task.findFirst({
where: {
id: req.params.taskId,
projectId: req.params.projectId,
project: { organisation: { members: { some: { userId: req.user.id } } } },
},
});
if (!task) return res.sendStatus(404);
res.json(task);
});
The query only finds the task if it belongs to the stated project and the caller is a member of that project's organisation. There is no path that loads an object first and checks later, which is where checks get forgotten.
Make the safe way the easy way
Repeating membership conditions in every query invites mistakes. Better options:
- Scoped repositories: a data access layer created per request with the caller's identity, whose methods always apply tenant and membership filters.
- Policy functions: a central
can(user, action, resource)check called by every handler, with one implementation to review and test. - Database row level security: PostgreSQL policies that restrict rows by the current tenant as a second layer.
Property and function level checks
Two related flaws complete the picture:
- Object property level: a viewer may read a task but must not see internal cost fields, and must not be able to change the
assigneefield even if they may edit the description. - Function level: only admins may call
DELETE /projects/:idorPOST /members/:id/role. Admin routes on the same API surface as user routes are a common source of this flaw.
Tests for every resource
For each resource type, write tests for the denial cases:
- A user from another organisation receives 404.
- A viewer cannot update.
- A non admin cannot call admin functions.
A short, consistent set of these tests across the API catches most regressions before they ship.