Authorization
Broken access control is the #1 risk on the web, and you can't test every endpoint × every role. So instead, we watch the decisions in production — and catch the ones that don't belong.
What you'll learn
- ✓Why authorization is impossible to test exhaustively
- ✓Catch IDOR enumeration and sudden escalation from traffic alone
- ✓Add one event for the precise detections traffic can't see
- ✓Fix the code: export decisions, and close a role-change race
Authorization
Authentication proves who you are. Authorization decides what you're allowed to do once you're in — and it's where apps get broken most. It's OWASP's #1 risk: broken access control. This episode models it, catches the attacks live, and fixes the code — most of it without writing a line.
The bugs developers miss
- IDOR / BOLA — change the id in the URL, read someone else's object. The app checked you're logged in; it forgot to check the object is yours.
- Broken function access (BFLA) — a new admin endpoint ships without its role gate; a regular member just calls it.
- Mass assignment — you send
role=adminin the body, and a careless handler saves it.
One root cause: a check that should be there, and isn't.
Why you can't just test it
The surface is combinatorial: every endpoint × every role × every object you might or might not own. Twenty endpoints and three roles is already 100+ cases — and every new endpoint multiplies it. Worse, the bug is a missing check: it throws no error and has no failing test. A green suite proves the happy path works; it says nothing about the door nobody locked.
So instead of enumerating that matrix, we watch the decisions in production and alarm on the shapes that don't belong.
Step 1 — Threat-model authorization
npx securenow threat-model authorization
It scores 71 threats against your code — from IDOR all the way to a category that barely existed two years ago: AI agents. For a small app most won't apply (no shared objects, no tenants, no agents yet), and the report says so plainly instead of inventing risk. What's left is the handful that are real.
For a well-gated app the main finding isn't a hole — it's blindness: a successful bypass looks exactly like a normal 200. The app records its access decisions in its own database, but SecureNow never sees them.
Step 2 — The three detection tiers
Pick the cheapest tier that works:
- Tier 1 — from the traffic alone, no app code. Object/ID enumeration, a
403burst across restricted routes, and the escalation detector below. - Tier 2 — one event per decision. For what traffic can't see (an authorized
200on an id the attacker already knew). - Tier 3 — the code fix. A detection contains the attacker; the missing check is still yours to write.
Step 3 — Catch sudden escalation with zero code
The question everyone asks: do we catch a user who suddenly escalates? Yes — from the traffic alone. The same client, on the same restricted route, is first denied (403) and then allowed (200). A door that was closed and is now open — a role granted mid-session, a stolen admin cookie, a gate that regressed. It's about fifteen lines of SQL:
SELECT ip, route, denied_at, allowed_at
FROM (
SELECT client_ip AS ip, route_template AS route,
minIf(ts, status = 403) AS denied_at, -- earliest deny
maxIf(ts, status = 200) AS allowed_at -- latest allow
FROM traces
WHERE route LIKE '/api/admin%' OR route LIKE '/api/security%'
GROUP BY ip, route
)
WHERE allowed_at > denied_at -- a door that opened
Step 4 — Add one event for the precise detections
For IDOR on ids the attacker already knows, traffic isn't enough — emit the decision:
track("authz.denied", {
userId, decision: "deny", reason: "role_denied",
route, have_role: "member", need_role: "admin",
target_ref: req.params.id, // who, what role, which object
});
That one line (the optional bridge from Episode 2, pointed at authorization) unlocks role-boundary probing, step-up bypass, and compromised-admin fan-out detections.
Step 5 — Make it fire, then attack
Create the rules in test mode, then attack your local copy: a member probes the admin surface (403 × 9), then escalates (403 → 200), then an admin session with no fresh password tries to change roles. Within the window SecureNow flags the step-up bypass, groups the probing, and catches the escalation from the traces — and the agent even proposes a block for a human to approve. Nothing fires on its own.
Step 6 — Fix the code
Two fixes the threat model wrote:
- Export the decisions —
track('authz.denied', …)at the gate, so probing is visible for good. - Close a race — two admins demoting each other at the same instant could leave the app with zero admins. Lock the admin set in one transaction and refuse the last one out:
BEGIN;
SELECT 1 FROM users WHERE role = 'admin' FOR UPDATE; -- lock the set
UPDATE users SET role = $1 WHERE id = $2
AND (SELECT count(*) FROM users WHERE role='admin' AND id<>$2) >= 1;
COMMIT;
Authorization in the age of AI
When you hand your app an AI agent or an MCP tool, that agent runs with its own broad identity — it can reach what the user can't (the confused deputy). So you re-authorize at the tool boundary: every call, on behalf of the real user, with a human in the loop for anything that matters. Same principle, new surface — and the threat model covers all 71 threats, including the eleven that are pure agent.
What you just did
You modeled authorization, caught privilege escalation and probing — most of it with zero app code — and fixed the two real holes. Model it. See it. Catch it. Fix it. The same loop, a new category. Now go run the threat model on your own app.
Try it on your own app
Start the threat model, then plug SecureNow in — the whole series works on your project today.