Back to path
BeginnerLedgerline · Project 2 of 12 ~6h· 5 milestones

Break and fix broken access control

Continues from the last build: Last rung you mapped how it could be attacked and stood up two test accounts on your own instance, but the threat model is still just a document. A01 Broken Access Control sits at the top of the list and nobody has actually tried it.

Your threat model ranked Broken Access Control as the number one risk, but a ranked list on a wiki page has never stopped an attacker.

IDOR discovery and evidence captureAuthentication vs authorization reasoningServer-side object-level authorization designFunction-level access control (BFLA) reviewThreat class remediation over single-instance patchingAutomated authz regression testingMapping findings to OWASP Top 10 and CWE

What you'll build

You will walk away with a captured, reproducible IDOR against your own instance, a server-side requireOwnership authorization layer applied to every account and transaction route (not a patch on the one URL you found), a fixed function-level access bug on an admin endpoint, and an automated regression test wired into CI that turns any future broken-access-control regression into a failing build instead of a leaked balance.

See how we teach, before you sign up

You don't just get code dumped on you. Every starter file and every solution is explained line-by-line, in plain English. Here's one real file from this project:

app/src/routes/accounts.tstypescript
import { Router } from 'express';
import { authenticate } from '../middleware/authenticate';
import { getAccountById } from '../db/accounts';

const router = Router();

router.get('/accounts/:id', authenticate, async (req, res) => {
  const account = await getAccountById(req.params.id);
  if (!account) {
    return res.status(404).json({ error: 'not found' });
  }
  res.json(account);
});

export default router;

Reading this file

  • router.get('/accounts/:id', authenticate, async (req, res) => {authenticate only proves who is asking, it never checks whether they are allowed to see this specific account id.
  • const account = await getAccountById(req.params.id);The lookup is keyed only on the URL param, so any authenticated user can pass any id and get a row back.
  • res.json(account);Whatever getAccountById returns, including another user's balance, is sent straight to the client with no ownership gate.

authenticate confirms a valid session exists but the handler never compares account.user_id to req.user.id, which is the whole vulnerability in one route.

That's 1 of 5 explained code blocks in this single project.

The build, milestone by milestone

  1. 1

    Find the IDOR and capture it as evidence

    5 guided steps

    You cannot fix a bug you have only read about in a threat model. Reproducing it against your own instance, with a saved request/response pair, turns 'A01 is our top risk' into 'here is the exact HTTP exchange that leaks a stranger's balance', which is what gets a fix prioritized and what you will use later to prove the regression test actually catches the old bug.

  2. 2

    Understand why authentication did not stop this

    4 guided steps

    If you patch the one route you found without understanding the root cause, you will fix the symptom and leave the same missing-ownership-check pattern in every other object route, which is exactly what the sibling-bug milestone will expose. Naming the class (missing object-level authorization, OWASP A01, CWE-639) is what lets you search the whole codebase for it instead of one file.

  3. 3

    Fix it with a real server-side ownership check

    5 guided steps

    Hiding the id, obscuring it, or checking ownership only in the one route you tested are all fake fixes, because the next route or the next developer who copies the pattern reintroduces the same hole. A shared, reusable ownership check applied at the data layer is the only version of this fix that survives someone adding a new account route next month.

  4. 4

    Find the sibling bugs from the same root cause

    5 guided steps

    A single patched route proves you can fix an instance; auditing every route proves you fixed the class. Support and admin tooling in particular tends to get bolted on later with 'it's only for support staff anyway' as the entire security model, which is exactly the missing-function-level-access-control pattern (OWASP A01 also covers this) that a real audit catches before an attacker does.

  5. 5

    Prove it with a test that fails on the old code

    5 guided steps

    A fix you verified manually once can silently regress the next time someone edits the routes file. A test that is proven to fail on the vulnerable version and pass on the fixed version is the only artifact that keeps this bug fixed six months from now when you are not the one reviewing the diff.

What's inside when you start

3 starter files, ready to clone
5 guided milestones
2 full reference solutions
5 code blocks explained line-by-line
5 "is it working?" checks
6 interview questions it prepares you for

You'll walk away with

A saved curl -i request/response transcript proving the original IDOR against your own instance
A requireOwnership middleware and updated data-layer functions applied to every account and transaction route
A requireRole('admin') check added to the previously unprotected admin route
An updated security/threat-model.md paragraph mapping the finding to OWASP A01 and CWE-639
An automated accessControl test suite proven to fail on the old code and pass on the fixed code
A CI job step running the authz test suite on every push and pull request

This is portfolio-grade. Build it free.

Sign up to unlock every milestone step-by-step, the code skeletons, full reference solutions, and checkable tasks, with your progress saved as you build.

Start building