The last lesson was about the front door: did the server really verify who you are? This one starts after that succeeds. The server knows you are Sam. The question now is whether Sam may read invoice 1002, delete a project, or call the admin API. A system that answers "yes" to all of those because Sam is logged in has missing authorization, and it is consistently among the most common serious flaws in real applications. Industry lists of web risks have ranked broken access control at or near the top.
Two questions, kept apart
- Authentication: who are you?
- Authorization: given who you are, may you do this to that?
Both are needed on every request. A hotel key card is a good picture. Authentication is the desk checking your ID before issuing the card. Authorization is the card itself only opening your own room and the gym, not the manager's office. If the card opened every door, the ID check at the desk would be pointless.
The common shapes
Object-level: changing the number. Your invoice loads at /invoices/1001. The server sees you are logged in and returns whatever invoice is named. You change the URL to /invoices/1002 and get a stranger's invoice. This is called an insecure direct object reference (). The fix is not hiding or randomising the number (though random IDs make guessing harder); it is checking, every time, that the invoice belongs to the requesting user or to a user they are allowed to act for.
Function-level: a button hidden, an endpoint open. The admin page hides its "Delete user" button from ordinary users, but the underlying call DELETE /api/users/7 still works for anyone logged in. Hiding an interface is a convenience, not a control. The server must check the role on the action itself.
Field-level. A profile update accepts {"name": "Sam", "role": "admin"} and saves every field it is sent. Only fields the user is allowed to change should be accepted.
Horizontal vs vertical. Reaching another user's data at the same privilege is horizontal. Gaining a higher privilege (user to admin) is vertical, and ties directly to privilege escalation in Level 4.
Scope not checked. Tokens often carry a scope: what they were issued to do, such as read:reports. A server that checks the token is genuine but not that its scope covers the requested action turns a narrow token into a skeleton key.

A real case
The product is JFrog Artifactory, self-hosted, a repository manager that organisations use to store and distribute software packages and build outputs. The record says versions before 7.133.11 are vulnerable to privilege escalation because the system validates a token's signature and issuer rather than its scope. In the language of this lesson, authentication passed (a genuine token from the right issuer) and the authorization step (does this token's scope permit this action?) was missing. The classification is CWE-863, incorrect authorization, and the CVSS vector (PR:L) says the attacker already needs a low-privilege account. The score is 8.8 (high). CISA added it to the Known Exploited Vulnerabilities catalogue on 11 September 2026.
Two lessons are worth drawing. First, a low-privilege starting point is not reassuring: escalation flaws turn the least powerful account into the most powerful, so every account on the system is a potential first step. Second, this is a software repository, a place many build pipelines trust. Anything that can alter what is stored there can affect what reaches the systems that depend on it, which is why repository managers are guarded closely. The record itself says nothing beyond the cause and the fixed version, and we add nothing to it.
Building authorization that holds
- Check on the server, every request. Never rely on a hidden button, a client-side role flag or a URL that "nobody knows".
- Deny by default. Access is granted by an explicit rule, not by the absence of a prohibition.
- Check the object, not only the user. Fetch by
id AND owner, so another user's record simply does not exist for the requester. - Check the action. Viewing, editing and deleting are different permissions. This is when you give permissions to roles and roles to people.
- Validate token scope, audience and expiry, not only the signature.
- Keep permission logic in one place, a function or policy layer, so it is reviewed once and not rewritten in a hundred handlers.
- Test across boundaries. For every endpoint, a test that logs in as user A and tries user B's object, and as an ordinary user tries an admin action. Both must fail.
- Log denials. A run of refused cross-user requests is exactly the signal that someone is trying numbers.
This is in code: the default answer is no, and each yes is narrow.

How AI changes this
Attackers. Access-control flaws depend on the logic of one specific application, which scanners could not judge and people had to study. Models that read an API specification or source and propose which object IDs and endpoints to try, then adapt based on responses, reduce that effort. Expect more of this kind of probing, faster. It cannot succeed against a check that is correctly enforced on the server.
Defenders. Generating the cross-user tests of step 7 for every endpoint is tedious for people and a good fit for AI assistance. Review support is useful too: ask a model to find handlers that fetch by ID without an ownership condition. Verify its findings; it can miss and it can invent.
Agents. This is where the new risk is sharpest. An agent acts with your authority, so every authorization decision made on "this is the user" is now made on "this is the user's agent, following instructions that may have come from a web page or a document". That is the problem: a trusted party tricked into using its authority for someone else. Agents also accumulate tokens, and an over-broad token for an agent is missing authorization by design. Give agents the narrowest scoped token, per task, and enforce limits on the server that the agent cannot argue its way past. Beam's write-up on enterprise data agents covers the permission side: enterprise data agent safety, permissions and injection.
Try it
A lab on your own machine, with a fake in-memory "database". Save as authz_lab.py.
INVOICES = {
1001: {"owner": "sam", "total": 120},
1002: {"owner": "alex", "total": 9800},
}
ADMINS = {"jo"}
# FLAWED: logged in is enough
def get_invoice_flawed(user, invoice_id):
return INVOICES.get(invoice_id)
# CHECKED: the object must belong to the user, or the user must be an admin
def get_invoice(user, invoice_id):
inv = INVOICES.get(invoice_id)
if inv and (inv["owner"] == user or user in ADMINS):
return inv
return None # same answer for "missing" and "not yours", so IDs are not leaked
# A scope check for tokens
def can(token, action):
return action in token["scopes"]
for user in ("sam", "alex", "jo"):
print(user, "asks for 1002 | flawed:", get_invoice_flawed(user, 1002), "| checked:", get_invoice(user, 1002))
read_only = {"scopes": ["read:reports"]}
print("read-only token may delete?", can(read_only, "delete:reports"))
Run it, then write three tests with assert that sam cannot read 1002, alex can, and the read-only token cannot delete. Finish by changing get_invoice to return None for a user that is None, i.e. not logged in. Deny by default means that the empty case is also covered.
Check yourself
Sam is logged in and changes /invoices/1001 to /invoices/1002 in the address bar. The server returns Alex's invoice. Which control is missing?
Authorization at the object level. The server authenticated Sam but never checked that invoice 1002 belongs to him. The fix is an ownership check on every fetch.
An admin button is hidden from normal users in the interface. Is the admin action protected?
No. A hidden button only hides the way in. The server must check the role on the underlying request, because anyone can send it directly.
Why is checking a token's signature and issuer not enough?
Those prove the token is genuine, not what it is allowed to do. Without checking scope, a narrow token can be used for broad actions. That mismatch is the cause in the CVE-2026-42016 record.
Why does the lab return the same None for a missing invoice and for someone else's?
Different answers would tell an attacker which IDs exist. Treating "not yours" the same as "not there" avoids leaking that.
Next
Authentication and authorization both depend on secrets: passwords, keys and tokens. Last lesson of this level: where those secrets actually end up, and how to find them before someone else does.

