Safeguard
Security Guides•

Broken access control lab: deny by default and object checks

A lab design for teaching authorization: find a missing object-level check, add a global deny-by-default rule, and test with two users in one session.

Safeguard Security Team
3 min read

An access control lab works best with two user accounts, an API that returns objects by id, and one missing check. Learners prove that user A can read user B's data, add the check, then add a deny-by-default rule so that the next endpoint is safe even if someone forgets.

Lab setup

Provide an orders API with two users and a handful of records each. Learners log in as user A and request an order that belongs to user B by changing the id. When it works, they have found the flaw, and the room usually goes quiet for a moment.

Give them the code for one endpoint and ask them to fix it before moving on.

What the guidance says

The OWASP Authorization Cheat Sheet lists the principles worth teaching:

  • Deny by default. The application should make an explicit decision to permit or deny, and prefer explicit configuration over framework defaults.
  • Check on every request. Include AJAX and server-side calls, and ideally apply checks globally instead of method by method. The page notes that an attacker only needs to find one way in.
  • Least privilege. Grant the minimum access a role needs, and review permissions periodically for creep.
  • Object-level checks. Hiding or randomizing identifiers is not sufficient. Verify access to the specific object on every request, since access to one object of a type does not imply access to others.
  • Server-side enforcement. Client-side checks may help the experience but should never decide access.

Exercise steps

  1. Exploit. Read another user's order by changing an id.
  2. Try the lazy fix. Switch ids to random strings. Let learners see that it only slows the attacker if the id ever leaks. This demonstrates the point about hidden identifiers.
  3. Fix properly. Add an ownership check in the data access path, so that a query for an order always includes the caller's identity.
  4. Add a global rule. Introduce middleware that denies any route without an explicit policy. Add a new endpoint with no policy and show that it returns forbidden.
  5. Test. Write a test that logs in as user A and asserts that user B's order returns forbidden, for every endpoint that takes an id.

Extending it

Add a role check for an admin-only report, and ask learners to find the client-side button hiding that is the only protection. They then move the check to the server.

For API specifics, see broken object level authorization explained.

Debrief questions

  • Which of our endpoints take an id, and which have a test for another user's id?
  • Do we have a default deny, or do new routes start open?
  • Who reviews permission changes?

Quick check

Are random identifiers an access control?

No. They make guessing harder but do not replace a server-side check of the caller's rights on each object.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.