Privilege Escalation in Express
How Privilege Escalation Manifests in Express
In an Express application, privilege escalation typically occurs when an endpoint that should be restricted to a privileged role (e.g., admin) can be accessed by a lower‑privileged user due to missing or flawed authorization checks. Because Express is unopinionated about authentication and authorization, developers often implement role checks ad‑hoc, which creates opportunities for bypass.
Common Express‑specific patterns include:
- Hard‑coded role strings in route handlers – a developer writes
if (req.user.role === 'admin') { … }but forgets to apply the same check to all HTTP verbs (GET, POST, PUT, DELETE) on the same path, allowing a privileged action via a less‑protected verb. - Missing middleware ordering – placing a role‑check middleware after a body‑parser or after a route that already performs sensitive logic means the request reaches the handler before the check runs.
- Over‑reliance on URL parameters for authorization – using
req.params.userIdto decide if a user can modify a resource without verifying that theuserIdmatches the authenticated user’s identity or that the user has the required role. - Prototype pollution leading to role manipulation – as seen in CVE‑2020-28477, a polluted
Object.prototypecan causereq.user.roleto evaluate to'admin'for any user when the code usesObject.assignor spread syntax without proper validation.
These issues map directly to the OWASP API Top 10 category BFLA (Broken Function Level Authorization) and are exactly what middleBrick’s “BFLA/Privilege Escalation” check looks for during its unauthenticated black‑box scan.
Express‑Specific Detection
Detecting privilege escalation in Express requires observing how the application responds to requests that vary only in the claimed role or user identifier. middleBrick performs this by:
- Scanning the target URL without any credentials (black‑box) and issuing a series of probes that modify headers, query strings, or JSON bodies to simulate different privilege levels (e.g., adding a custom
X-User-Role: adminheader or alteringuserIdparameters). - Comparing the responses: if a request that should be forbidden (e.g., a DELETE to
/admin/users/123) returns a success status (2xx) for a low‑privilege probe while the same request with a legitimate admin token returns a success, the scanner flags a potential BFLA. - Checking for inconsistent verb handling: the scanner sends GET, POST, PUT, PATCH, and DELETE to the same endpoint and looks for any verb that bypasses a role check present on others.
- Looking for signs of prototype pollution: it sends payloads designed to pollute
Object.prototype(e.g.,{"__proto__":{"role":"admin"}}) and monitors whether the application’s behavior changes in a way that elevates privileges.
Because middleBrick runs 12 checks in parallel, the privilege‑escalation test completes within the 5‑15 second window alongside the other scans (authentication, BOLA/IDOR, input validation, etc.). The resulting report includes a severity rating, a short description of the vulnerable path, and remediation guidance that references Express‑specific fixes.
You can invoke this check from the CLI with:
middlebrick scan https://api.example.comOr add it to your CI pipeline via the GitHub Action:
- name: Run middleBrick security scan uses: middlebrick/action@v1 with: api-url: https://staging.example.com fail-below: B # fail the job if score drops below BThe same scan can be launched directly from an AI coding assistant using the MCP Server integration, allowing developers to see findings without leaving their IDE.
Express‑Specific Remediation
Fixing privilege escalation in Express relies on applying consistent, centralized authorization middleware and validating that role information cannot be tampered with. Below are practical, Express‑native approaches.
1. Centralize role checks with middleware Place a single middleware that verifies the user’s role before any route handler runs. This guarantees that all verbs and all paths under a given router share the same check.
// authMiddleware.js function requireRole(allowedRoles) { return function (req, res, next) { if (!req.user || !req.user.role) { return res.status(401).json({ error: 'Unauthenticated' }); } if (!allowedRoles.includes(req.user.role)) { return res.status(403).json({ error: 'Forbidden: insufficient role' }); } next(); }; } module.exports = { requireRole };Then use it in your route definitions:
const express = require('express'); const { requireRole } = require('./authMiddleware'); const router = express.Router(); // All admin routes protected by the same middleware router.use('/admin', requireRole(['admin'])); router.get('/admin/users', (req, res) => { // safe to assume req.user.role === 'admin' res.json({ users: getAllUsers() }); }); router.delete('/admin/users/:id', (req, res) => { deleteUser(req.params.id); res.status(204).send(); }); module.exports = router;2. Validate the source of role information If you rely on a JWT or session, ensure the role claim is verified by the signature and never accepted from untrusted inputs like query strings or headers. Example using
express-jwt:const jwt = require('express-jwt'); const jwksRsa = require('jwks-rsa'); const checkJwt = jwt({ secret: jwksRsa.expressJwtSecret({ cache: true, rateLimit: true, jwksUri: 'https://YOUR_AUTH0_DOMAIN/.well-known/jwks.json' }), audience: 'YOUR_API_IDENTIFIER', issuer: `https://YOUR_AUTH0_DOMAIN/`, algorithms: ['RS256'] }); app.use(checkJwt); // after this, req.user is guaranteed to be signed by the IdP3. Avoid prototype‑pollution vectors Never merge user‑provided objects directly into objects that hold security‑relevant data. Use a whitelist or a library like
lodash.mergeWithwith a customizer that ignores__proto__andprototypekeys.const _ = require('lodash'); function safeUpdateUser(update) { // Only allow specific fields const allowed = ['name', 'email']; const filtered = _.pick(update, allowed); return _.assign(req.user, filtered); } app.put('/profile', (req, res) => { safeUpdateUser(req.body); res.sendStatus(200); });4. Leverage Express Router for scoped middleware Define a separate router for each privilege level and mount it under a path that reflects that level. This makes it harder to accidentally expose a privileged route without the intended middleware.
const adminRouter = express.Router(); adminRouter.use(requireRole(['admin'])); adminRouter.get('/stats', (req, res) => { /* … */ }); app.use('/api/admin', adminRouter);By applying these patterns, you close the most common Express‑specific privilege‑escalation routes that middleBrick would flag, turning a potential BFLA finding into a clean A‑grade score.
Frequently Asked Questions
Does middleBrick modify my Express code to fix privilege‑escalation issues?
Can I use the middleBrick CLI to scan an Express API that runs locally on port 3000?
middlebrick scan http://localhost:3000 from your terminal. The CLI will send the same set of probes as the web dashboard and return a JSON or text report you can integrate into scripts or CI pipelines.