Privilege escalation
CWE-269OWASP A01:2021Updated August 31, 20266 min read
Privilege escalation is a vulnerability that lets a user obtain rights they were never granted, usually those of an administrator. It happens when an application takes the role from the request or fails to guard an admin route separately. The fix is to resolve the role server side from the session, on every single request.
Almost every application recognises several kinds of user: a customer, an employee, an administrator. As long as the application itself decides who is what, that works. The moment it lets the incoming request decide, or leaves the admin side unguarded, an ordinary user can promote themselves. Here is how privilege escalation arises and what to do about it.
What is privilege escalation?
Privilege escalation is a vulnerability in which a user obtains rights that were never granted to them, because the application takes their role from an untrusted source or fails to guard a privileged function separately. Testers routinely shorten privilege escalation to privesc.
An everyday comparison: a visitor badge on which you write your own job title. At the boardroom door the guard looks only at the badge, never at the HR system. Write “director” on it and you walk straight in.
Practitioners distinguish two directions. In horizontal escalation a user reaches a peer’s data, which amounts to an IDOR. In vertical escalation, the subject of this article, the user climbs a level, usually straight to administrator. In the OWASP Top 10 both fall under A01:2021, Broken Access Control; MITRE tracks the underlying flaw as CWE-269, Improper Privilege Management.
Two causes account for most findings: a role copied straight out of the request, and an admin route that is hidden but not protected.
How does a privilege escalation attack work?
An attacker needs nothing more than a legitimate account. If a response contains a field such as role, isAdmin or permissions, they send it back in their next request.
Vulnerable:
// PATCH /api/profile: the user updates their own profile
app.patch("/api/profile", requireLogin, async (req, res) => {
// The entire request body goes to the database unfiltered
await db.users.update({ id: req.session.userId }, req.body);
res.json({ ok: true });
});
// GET /admin/users: the administrator overview
app.get("/admin/users", requireLogin, async (req, res) => {
// Being logged in is enough; the role is never checked
res.json(await db.users.findAll());
});
The first route writes everything it receives into the user record: the developer was thinking of a name and a phone number, but the role field is accepted too. The attacker sends this:
PATCH /api/profile HTTP/1.1
Host: app.example.com
Content-Type: application/json
Cookie: session=8f2a4c1b
{"displayName":"Mark","role":"admin"}
The response is an ordinary 200, and from now on the application reads the administrator role from its own database. This variant is known as mass assignment: a framework that binds fields to an object automatically also binds the ones the user was never meant to set.
The second route shows the other pattern: it checks that someone is logged in, but not who they are. The path is absent from an ordinary user’s menu, and that is what the developer relies on. Attackers find such paths in the JavaScript bundle, which often ships the admin screens too, or with a list of common names. This is called forced browsing.
The secure version reverses both: the write side accepts only what a user may set about themselves, and the read side resolves the role server side.
Secure:
const PROFILE_FIELDS = ["displayName", "locale", "phone"];
app.patch("/api/profile", requireLogin, async (req, res) => {
// Allowlist: only these fields reach the database, role is not among them
const patch = {};
for (const field of PROFILE_FIELDS) {
if (field in req.body) patch[field] = req.body[field];
}
await db.users.update({ id: req.session.userId }, patch);
res.json({ ok: true });
});
// The role comes from storage, keyed on the session, on every request
function requireRole(role) {
return async (req, res, next) => {
const user = await db.users.findById(req.session.userId);
if (!user || !user.roles.includes(role)) return res.status(404).end();
next();
};
}
// Deny by default across the whole admin tree, not per individual route
app.use("/admin", requireLogin, requireRole("admin"));
app.get("/admin/users", async (req, res) => {
res.json(await db.users.findAll());
});
The allowlist makes it impossible for an unknown field to reach the database. The role check loads the user from storage on every request and inspects the roles held there, not something the client sent. Because the check hangs on the whole admin tree, a new route is protected before anyone remembers to think about it, and answering with 404 rather than 403 avoids revealing which admin paths exist.
What is the impact of privilege escalation?
Vertical escalation hands an attacker the keys to the application: read and export all customer data, create and delete accounts, alter invoices. The first move is often a second admin account, so access survives the closing of the original hole.
It rarely stops at the application. Admin screens offer functions that reach deeper: a file upload, a template editor, a setting that invokes a system command. Such functionality is tested less rigorously, precisely because only trusted staff were supposed to reach it, which makes it a stepping stone to remote code execution.
In business terms: a reportable data breach, fraud wherever payments can be altered, and an audit trail that loses its value because actions are logged under an apparently ordinary user. Severity ranges from high to critical, depending on how far the stolen role reaches. A user who becomes moderator of their own team is a nuisance; the same flaw where the admin role spans every customer is far worse.
How do you detect privilege escalation?
Start with the fields the application returns about a user: request your own profile and note anything that smells of permissions, such as role, isAdmin or permissions. Send those fields back in an update, even when the form does not show them, and see whether they stick. Do the same on registration, which often accepts a complete object.
Next, test the routes themselves. Record an admin account’s traffic, replay every request with an ordinary user’s session and note anything answered with something other than 401 or 403; Burp Suite with the Autorize extension does this systematically. Look for admin paths in the JavaScript bundle and in older API versions, and switch methods too: sometimes the GET is guarded and the POST on the same path is not.
Scanners find an admin page reachable without logging in, but they do not know your role model and cannot tell that user B may do what only administrator A was allowed to do. That remains manual work. AssistSec covers this in a penetration test, with accounts at different privilege levels as a matter of course.
How do you prevent privilege escalation?
- Resolve the role server side from the session. Read it from your own storage on every request and never accept a role or permission list from the client.
- Use an allowlist of writable fields. Never bind a request body straight to your data model; state explicitly which fields a user may set about themselves.
- Deny by default. Attach authorization to the whole admin tree instead of individual routes, so a new endpoint is protected before anyone thinks about it.
- Check per action, not per screen. Hiding a button is not authorization; every call behind that screen needs its own check.
- Re-authenticate for heavy actions. Granting a role or changing an email address deserves an extra authentication step.
- Keep permissions current and tokens short. Make sure a revoked role takes effect immediately and that you can revoke sessions and tokens actively.
- Log role changes and test the scenario. A user who makes themselves an administrator should raise an alert; an integration test that requests the admin routes with account B prevents regressions.
Sources
Frequently asked questions
What is the difference between horizontal and vertical privilege escalation?
In horizontal escalation a user reaches the data of a peer, which is essentially an IDOR. In vertical escalation the user climbs a level, usually to administrator. Vertical escalation normally weighs heavier, because admin functions act on every user rather than on one account. Both sit under A01:2021, Broken Access Control, in the OWASP Top 10.
Is privilege escalation the same as broken access control?
Not quite. Broken access control is the umbrella category in the OWASP Top 10; privilege escalation is the outcome in which a user ends up at a higher permission level. IDOR and mass assignment are other expressions of the same category.
Does a JWT protect against privilege escalation?
Only if you verify it properly. A signed token stops the client from editing the role in the payload, but it does not help when your server accepts the none algorithm or skips signature verification. A role inside a token also stays valid until it expires, so without revocation a withdrawn admin role keeps working.
Why is a hidden admin page not a security control?
Because the path is discoverable. Admin routes are often shipped in the JavaScript bundle that every visitor downloads, and otherwise an attacker finds them with a list of common names. Only a server-side check on every request actually stops them.
Related articles
- GlossaryPentestA penetration test (pentest) is a controlled attack on your systems by ethical hackers. Learn how a pentest works and what vulnerabilities it uncovers.
- VulnerabilitiesCWE-284A01:2021Broken access controlBroken access control explained: horizontal and vertical privilege escalation, forced browsing, and how deny by default fixes it server-side.
- VulnerabilitiesCWE-287A07:2021Broken authenticationBroken authentication explained: how attackers take over accounts through brute force, leaked passwords and predictable session tokens, and how to stop them.
- VulnerabilitiesCWE-639A01:2021Insecure direct object reference (IDOR)IDOR explained: how attackers tamper with IDs in URLs or APIs to read or change other users' data, and how to detect and prevent this vulnerability.
- VulnerabilitiesCWE-915A01:2021Mass assignmentMass assignment explained: how binding a whole request body onto a model makes isAdmin or balance writable, and how an allowlist prevents it.