A food-delivery platform in the mould of Zomato has a restaurant dashboard. A restaurant owner logs in and edits their menu with PUT /api/menus/8841. Changing 8841 to another restaurant's id in the URL succeeds. Separately the refund endpoint POST /api/admin/refunds is hidden in the owner UI but works when called directly by any logged-in owner. Which two OWASP API risks are these?
Show the full answer Hide the answer
What is being tested
Whether you can separate two authorization failures that are often lumped together. Both pass authentication; they fail at different questions.
The mechanism
Object level (API1:2023 in the OWASP API Security Top 10): the caller is allowed to use this endpoint, but not on this object. The server checks that the user is a logged-in restaurant owner and never checks that menu 8841 belongs to that owner. Changing the id is all an attacker needs, and sequential ids make enumerating every menu a loop. The fix is an ownership check on every object access, ideally enforced in the data layer (WHERE id = :id AND restaurant_id = :caller_restaurant) so a forgotten check in a handler cannot expose rows.
Function level (API5:2023): the caller is not allowed to use this endpoint at all. The refund route exists for finance staff; the owner UI hides the button, and hiding a button is not an access control. The fix is a role or permission check at the route, denied by default, with admin routes registered in a separate, separately authorised group.
The difference matters operationally: the first is fixed per object lookup, the second per route, and a scanner finds them with different tests (swap ids versus call privileged routes as a low-privilege user).
Why the other options fail
- Broken authentication for both is wrong because the login works. The owner is who they claim to be; the failure is what they are permitted to do.
- Injection and misconfiguration describe a different class of flaw (untrusted input reaching an interpreter; insecure defaults). Nothing here is injected, and the refund route is not misconfigured, it is unauthorised.
- Resource consumption and improper inventory management are real API risks, but the menu edit is not about volume and the refund route is not forgotten or undocumented; it is known and unprotected.
What to do first
- Add an authorisation test per route that calls it as a user of another tenant and as a low-privilege role, and fails the build on 200.
- Replace guessable sequential ids with random identifiers as a second layer; this slows enumeration and does not replace the ownership check.
- Log denied attempts; a burst of 403s across many ids is the signature of enumeration.
When extra machinery is not needed
A small internal tool with one trust level and no tenants does not need a policy engine. A consistent middleware check per route, tested, is enough. Reach for relationship-based or policy-as-code systems when rules depend on relationships between users and objects that change.
Common weak answers
- "Hide the admin routes." Obscurity is discoverable from the client bundle.
- "Use UUIDs and the problem goes away." The check is still missing; ids only add friction.