What is privilege escalation? Examples explained

What is privilege escalation? Examples explained

Most web apps decide what you can see and do based on who you are. When an attacker breaks that decision and gains rights they were never granted, that is privilege escalation. It is one of the most common and most damaging classes of bug in modern web apps, and it usually hides in plain sight inside ordinary features.

The good news is that the idea is simple once you see a few examples. Below we walk through what the bug looks like, the two main flavors, and how to spot and stop it in your own app.

What is privilege escalation?

Privilege escalation happens when a user performs an action or reads data that their account should not be allowed to touch. The app trusts the request without checking, on the server, whether this specific user is allowed. The attacker does not break the login. They log in as themselves and then reach further than their account permits.

Think about a typical SaaS app we will call Acme Notes. Every user has a role, and every note has an owner. The rules are clear on paper. A regular member can edit their own notes. An admin can manage every account. Privilege escalation is what happens when the code never enforces those rules on each request.

Authentication proves who you are. Authorization decides what you may do. Privilege escalation is the gap that opens when the second check is missing or wrong.

The two kinds: horizontal and vertical

Almost every case fits into one of two shapes. Both come from the same root cause, a missing server side check, but they reach different targets.

Horizontal escalation: acting as another user at the same level

Horizontal escalation means you stay at your own permission level but act as a different account at that same level. You are a member, and you reach into another member’s data.

In Acme Notes, suppose the app loads a note like this:

GET /api/notes/8841
Authorization: Bearer (your real token)

You own note 8841. Out of curiosity you change the number:

GET /api/notes/8842

If the server returns note 8842 and it belongs to someone else, the app never checked ownership. It saw a valid login and trusted the request. That is a classic example, often called an insecure direct object reference. The same flaw shows up on profile pages such as /api/users/1207/settings, on invoices, on file downloads, and anywhere an identifier appears in the URL or body.

Vertical escalation: becoming an admin

Vertical escalation means you climb to a higher permission level than your account should have. A member becomes an admin. Here are two simple invented examples.

  • Flipping a role field. Imagine the signup or profile update endpoint accepts the whole user object and saves every field it receives. You send your normal update but add one line:
    PATCH /api/users/me
    {
      "displayName": "Sam",
      "role": "admin"
    }

    If the server saves role straight from the request body, you just promoted yourself. This is a mass assignment bug, and it turns a profile form into an admin switch.

  • Hitting an admin only endpoint directly. The admin dashboard link is hidden from your navigation bar, so it feels protected. But the button only hides the link, it does not guard the route. You guess or read the path and call it yourself:
    POST /api/admin/users/3092/delete

    If the server runs the action because you are logged in, without checking that you are an admin, the hidden link was the only lock on the door.

How it connects to broken access control

Privilege escalation is the practical result of broken access control. Access control is the set of rules about who can do what. When those rules are checked in the browser only, or checked for some routes but forgotten on others, or written so that any logged in user passes, the control is broken. An attacker walks straight through the gap.

The pattern repeats across apps because the checks are scattered. One endpoint verifies ownership, the next one nearby does not. A new feature ships without the guard the older feature had. You can read more in our access control category, where this family of bugs lives.

How to spot it

You find these bugs by questioning what the app assumes about you, then testing each assumption with a real request. A few concrete checks:

  • Change the identifier. Take any request with an id in the path or body and swap it for an id you do not own. If you get data back, you found horizontal escalation.
  • Add fields the form does not show. Send role, isAdmin, accountType, or ownerId in an update request and see if the server keeps them.
  • Call privileged routes as a low rights user. List every admin endpoint you can find and request each one with a plain member token. A 200 OK where you expected 403 is the bug.
  • Compare two accounts. Log in as a member and as an admin. Watch which checks the server applies to one and skips for the other.

The mindset matters more than any single test. You are not throwing known payloads at the app. You are reading how the app expects to be used, then asking what happens when you step outside that expectation.

How to prevent it

Every fix comes back to one rule: check authorization on the server, for every request, against the user making it.

  • Check ownership and role on each request. Before returning note 8842, confirm the note’s owner matches the logged in user. Before running an admin action, confirm the caller is an admin. Do this on the server, never in the browser alone.
  • Deny by default. New routes should reject access until you explicitly allow it. A forgotten guard should fail closed, not open.
  • Never trust client supplied fields for permissions. Read role and ownerId from your database record for the session, not from the request body. Allow list the fields an update may change.
  • Use one shared authorization layer. When every route calls the same access check, you stop the slow drift where one endpoint is safe and the next one is not.
  • Test the negative case. Write tests that confirm a member gets 403 on admin routes and cannot read another member’s data. Run them on every change.

Privilege escalation rarely announces itself. There is no crash and no error in the logs, just a request that succeeded when it should have failed. That quietness is exactly why testing the assumptions an app makes finds these bugs when a fixed list of payloads will not. It is the kind of flaw an autonomous researcher built to understand an app, form ideas about where its logic breaks, and verify each finding with real evidence is made to catch. If you want to see how we think about this, read more about UnboundCompute.