Privilege escalation is what happens when an account ends up able to do something its permission level was never meant to allow. In web applications it is rarely one dramatic exploit. It is usually a small gap in how permission is assigned, cached, or trusted, reached by a request that looks entirely ordinary. These privilege escalation examples come from an invented workspace app, each with the request that causes it and the reason the check failed.
Horizontal and vertical escalation
Two directions are worth naming before the examples. Horizontal escalation means acting as a different account at the same level, such as one member reading another member’s records. Vertical escalation means gaining a higher level, such as a member becoming an administrator. They matter separately because a chain often runs horizontally first and then vertically: take over any account, discover that one of those accounts is an admin, and the second step is free.
Five privilege escalation examples
Acme Notes is an invented team workspace with members, administrators, and a support tool. Every request below is authenticated and well formed.
1. Promoting yourself through a profile update
The profile endpoint saves whatever fields arrive, because it was written to be flexible about which ones the form sends.
PATCH /api/users/me
Authorization: Bearer tokenForMember
{ "display_name": "Sam", "role": "admin" }
200 OK
{ "id": 12, "display_name": "Sam", "role": "admin" }
The endpoint is the caller’s own record, so an ownership check passes. What is missing is a rule about which attributes a caller may write to their own record. Permission fields must be server decided, and a handler that binds a whole request body to a model will not know the difference.
2. Choosing your role when accepting an invite
An invitation is emailed with a token, and the acceptance endpoint reads the role from the request rather than from the invitation record.
POST /api/invites/accept
{ "token": "inv_9f3c...", "role": "owner" }
201 Created
{ "workspace_id": 7, "user_id": 4310, "role": "owner" }
The invitation said member. The server never compared the two, so the invited person picks their own level. The same pattern shows up wherever a value that was decided earlier is resent by the client later, including plan tiers, seat counts, and approval states.
Most escalation bugs are not a broken permission check. They are a permission that the server let the client supply in the first place.
3. Escalating by taking over a higher privileged account
The email change endpoint updates the address immediately and sends a verification link afterwards, and password reset uses the current address on file.
PATCH /api/users/88/email
Authorization: Bearer tokenForMember
{ "email": "attacker@example.com" }
200 OK
{ "id": 88, "email": "attacker@example.com", "verified": false }
Two failures compound here. The endpoint took an id from the path without checking it belongs to the caller, and the account switched to an unverified address that password reset still trusts. Neither is an escalation on its own. Together they turn any member into whichever account they choose, and account 88 happens to be an administrator.
4. Permissions that outlive the change
An administrator is demoted to member. Their existing token still carries the old claims, and the service reads role from the token rather than from the database.
GET /api/admin/users
Authorization: Bearer tokenIssuedBeforeDemotion
200 OK
{ "users": [ ... ] }
The permission model is correct and the enforcement is stale. Any place that caches authorization, such as long lived tokens, a session copy of the role, or a permissions list computed at login, keeps granting access after the decision behind it changed. Offboarding is where this hurts most.
5. A support tool with no separate guard
Support staff can view an account as its owner to reproduce issues. The impersonation endpoint checks that the caller is signed in and assumes only staff can reach it, because only staff see the button.
POST /api/support/impersonate
Authorization: Bearer tokenForMember
{ "user_id": 88 }
200 OK
{ "session": "eyJ...sessionAsUser88" }
Internal features are frequently built with lighter checks than customer facing ones, on the assumption that only internal people will call them. Impersonation, feature flag toggles, data export, and replay tools are worth reviewing first, because each one converts a normal account directly into another account.
How to test for privilege escalation
- Hold accounts at every level. Two members, one admin, and, if the product has them, one support account. Escalation testing is comparison testing and needs something to compare.
- Replay privileged traffic downward. Record what the admin account does, then send exactly those requests with a member token. Anything that does not return a denial is a finding.
- Add permission fields to bodies that do not document them.
role,is_admin,plan,scopes,owner_id, andworkspace_idare the usual candidates. - Change permissions and keep using the old session. Demote an account, then reuse its token. Revoke a seat, then call the API again. This catches the stale authorization class that point in time testing misses.
- Look for the second step. An account takeover is only medium severity until you check whether any reachable account is privileged. Chains are where the real impact sits.
None of these are found by matching a payload, because there is no payload. They are found by knowing which accounts exist, what each is meant to be able to do, and then checking whether the server agrees. More on access control bugs is here.
How to prevent it
- Allowlist writable fields per endpoint, so permission attributes cannot be set by a request even if they appear in one.
- Read authority from the record, not the request. The invitation stores the role, so acceptance should use the stored value and ignore anything sent alongside the token.
- Check authorization at the moment of use. If you must cache it, keep token lifetimes short and give yourself a way to revoke immediately.
- Verify an email before it becomes the account’s identity, and invalidate active sessions and reset tokens whenever the address or password changes.
- Guard internal tools like external ones. Impersonation deserves its own permission, an audit record, and ideally a second factor.
- Deny by default, so a new route is unreachable until someone declares who may call it.
Privilege escalation tends to be assembled rather than discovered: a writable field here, a stale token there, an id that was never checked, combined into a path from ordinary member to full control. Following that chain requires understanding how an application’s roles are meant to fit together, which is exactly what an autonomous researcher that reasons about application logic is built to do. Read more about how UnboundCompute works.
Frequently asked questions
What is an example of privilege escalation in a web application?
The most common one is a writable permission field. A member sends PATCH /api/users/me with { "display_name": "Sam", "role": "admin" }, the handler binds the whole body to the user model, and the account is now an administrator. The ownership check passed, because the record really does belong to the caller. What was missing is a rule about which attributes a caller may write to their own record.
What is the difference between horizontal and vertical privilege escalation?
Horizontal means acting as another account at the same permission level, such as one member reading or editing another member’s records. Vertical means gaining a higher level, such as a member reaching administrator functions. Real incidents usually chain them: an attacker moves horizontally into any account they like, then checks whether one of those accounts is privileged, which makes the vertical step free.
Can privilege escalation happen even when permissions are configured correctly?
Yes, and stale authorization is the usual reason. If a service reads the role from a long lived token or from a copy stored in the session, an account that was demoted keeps its old access until that token expires. The permission model is right and the enforcement is out of date. This is why offboarding tests matter: change a permission, then keep using the session that was issued before the change.
How do I test my application for privilege escalation?
Hold accounts at every level, then compare. Record the requests an admin account makes and replay them with a member token, and treat anything other than a denial as a finding. Add fields such as role, is_admin, and scopes to bodies that do not document them. Demote an account and reuse its old token. Finally check whether any account you can take over is itself privileged, because that is where a medium severity bug becomes a critical one.
Put an autonomous researcher on your own systems
UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.
