Juicy Shop
Broken Access Control & IDOR
Broken access control is the #1 web risk. Learn how trusting client-supplied identifiers leads to IDOR, why every request must be authorised server-side, and how to enforce ownership checks.
Authorisation is per-request, server-side
Authentication establishes identity once; authorisation must be checked on every single request that touches protected data or actions. The cardinal sin is trusting the client to tell you who it is or what it owns. Hiding a button in the UI is not access control — the attacker calls the API directly.
IDOR: Insecure Direct Object Reference
IDOR occurs when an endpoint uses a client-supplied identifier to select a resource without verifying the caller is allowed to access it. Change /orders/1001 to /orders/1002 and you read someone else's order; submit a review with another user's authorId and you post as them. The reference itself isn't the bug — the missing ownership check is.
Derive identity from the session, not the body
Never accept the acting user's identity from request data. Derive it from the authenticated session on the server. For the forged-review challenge, the fix is simply const authorId = session.user.id — the client-supplied authorId is ignored entirely. For resources, verify that the session user owns (or has a role permitting) the target object before reading or mutating it.
Design for safe defaults
Deny by default: a request is unauthorised unless a rule explicitly allows it. Centralise checks in middleware or a policy layer so they cannot be forgotten on a new endpoint. Use unguessable identifiers (UUIDs) as defence in depth, but never as a substitute for an actual authorisation check.
Insecure vs secure
const session = await getSession(request);
const body = await request.json();
const authorId = body.authorId;
await sql`insert into reviews (author_id, text)
values (${authorId}, ${body.text})`;The author is taken from the request body, so an attacker can set authorId to any user and forge reviews in their name (IDOR).
const session = await getSession(request);
if (!session) return unauthorized();
const body = await request.json();
const authorId = session.user.id;
await sql`insert into reviews (author_id, text)
values (${authorId}, ${body.text})`;Identity is derived from the authenticated session and the client-supplied value is ignored, so a review can only ever be attributed to the real caller.
Key takeaways
- Access control is the #1 OWASP risk — check authorisation on every request.
- IDOR is a missing ownership check, not the identifier itself.
- Derive the acting user from the session; never trust client-supplied IDs.
- Deny by default and centralise checks so endpoints can't forget them.
Knowledge check
Practice
Apply this lesson hands-on in the matching find-it / fix-it challenge.
Open challenge →