Course page
Log in

You are watching a free preview. Enroll to open all 24 lessons, track progress and ask the course tutor.

Enroll for $59.99

Section 1: API security foundations

Object level authorisation in practice

Article 12 min Lesson 2 of 24 Preview

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 assignee field even if they may edit the description.
  • Function level: only admins may call DELETE /projects/:id or POST /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.

← → switch lessons