Broken access control has been the #1 vulnerability class in the OWASP Top 10 since 2021 — overtaking injection for the first time after a decade. It's also one of the hardest to catch automatically, because it requires understanding what a user should be allowed to do, not just what they can do.
Here's what it looks like in practice and how to systematically detect it.
Pattern 1: Insecure Direct Object Reference (IDOR)
IDOR is the most common broken access control pattern. An endpoint accepts an ID from the user and returns the corresponding resource — without checking whether the requesting user owns or has permission to access that resource.
// Vulnerable: no ownership check
app.get('/api/invoices/:id', async (req, res) => {
const invoice = await db.invoice.findUnique({
where: { id: req.params.id }
})
res.json(invoice)
})
// Fixed: check ownership
app.get('/api/invoices/:id', async (req, res) => {
const invoice = await db.invoice.findUnique({
where: { id: req.params.id, userId: req.user.id }
})
if (!invoice) return res.status(404).json({ error: 'Not found' })
res.json(invoice)
})
The attack is trivial: increment or enumerate the ID. An attacker accessing /api/invoices/1, /api/invoices/2, /api/invoices/3 gets every invoice in your database if there's no ownership check. Use UUIDs instead of sequential integers to reduce the attack surface — but don't rely on ID unpredictability as a security control, since UUIDs still need an auth check.
Pattern 2: Missing authorization middleware
A new route gets added to the router, but the developer forgets to add the authentication middleware. Or the middleware exists but is applied to a parent route that doesn't cover this endpoint's path.
// Express: auth middleware applied to /api/users but not /api/admin
app.use('/api/users', authenticate, usersRouter)
app.use('/api/admin', adminRouter) // missing authenticate!
// Better: apply globally, opt out for public routes
app.use(authenticate)
app.use('/api/public', publicRouter) // explicitly public
Framework-specific patterns make this worse. In Next.js App Router, a new route.ts file in a protected directory may not automatically inherit middleware from middleware.ts depending on the path pattern. In Express, middleware order and path matching is a constant source of access control bugs.
Pattern 3: Privilege escalation via parameter tampering
The application trusts a user-controlled field to determine access level:
// Vulnerable: role comes from request body
app.post('/api/users', async (req, res) => {
const user = await db.user.create({
data: {
email: req.body.email,
role: req.body.role // attacker sets "admin"
}
})
})
// Fixed: server assigns role
app.post('/api/users', async (req, res) => {
const user = await db.user.create({
data: {
email: req.body.email,
role: 'user' // always assign default, never trust client
}
})
})
Mass assignment is a variant of this: Object.assign(user, req.body) or ORM patterns like User.create(req.body) let an attacker set any field the ORM model exposes, including role, isAdmin, or stripeCustomerId.
Pattern 4: Horizontal vs vertical privilege escalation
- Horizontal escalation: User A accesses User B's data at the same privilege level. The IDOR pattern above.
- Vertical escalation: A regular user accesses admin functionality. The missing middleware pattern above.
Both require different test strategies. Horizontal escalation is caught by testing with two accounts at the same privilege level. Vertical escalation is caught by testing protected endpoints without authentication or with a lower-privilege token.
How automated tools detect broken access control
Fully automating IDOR/BOLA (Broken Object Level Authorization — the OWASP API Security term for this class of vulnerability) detection requires dynamic testing — a scanner that can create two user sessions and verify that session A cannot access session B's resources:
- Crawler discovers all API endpoints and the ID shapes they accept
- Testing agent authenticates as User A, collects a list of resource IDs
- Testing agent authenticates as User B, attempts to access User A's resource IDs
- Any successful access (2xx response) is a confirmed IDOR finding
For code review, AI-driven analysis can flag the patterns: database queries that use req.params.id without a userId filter, routes mounted without authentication middleware, and create calls that spread req.body directly into a model.