A01 Broken Access Control
A01:2021 — Broken Access Control — is the top OWASP risk. The bug class is “the server let a user do something they shouldn’t”: IDOR, missing function-level checks, vertical/horizontal privilege escalation.
Authz patterns + IDOR defences
EXAMPLE
// 1) ANTI-PATTERN — trusting the client
app.get('/orders/:id', async (req, res) => {
const order = await db.orders.findOne({ id: req.params.id });
res.json(order); // ANYONE can read ANY order by guessing IDs
});
// 2) FIX — always check ownership on read
app.get('/orders/:id', requireAuth, async (req, res) => {
const order = await db.orders.findOne({ id: req.params.id });
if (!order || order.userId !== req.user.id) {
return res.status(404).end(); // 404, not 403 — don't reveal existence
}
res.json(order);
});
// 3) Or include the user in the WHERE clause (better — atomic)
app.get('/orders/:id', requireAuth, async (req, res) => {
const order = await db.orders.findOne({
id: req.params.id,
userId: req.user.id, // scoped query
});
if (!order) return res.status(404).end();
res.json(order);
});
// 4) Vertical privilege check — role-based
function requireRole(role) {
return (req, res, next) => {
if (req.user?.role !== role) return res.status(403).end();
next();
};
}
app.delete('/admin/users/:id', requireAuth, requireRole('admin'), deleteUser);
// 5) Resource-based policies (Casbin / ABAC)
const can = (user, action, resource) =>
enforcer.enforce(user.role, resource.type, action);
app.put('/posts/:id', requireAuth, async (req, res) => {
const post = await db.posts.findOne({ id: req.params.id });
if (!post || !can(req.user, 'edit', post)) return res.status(403).end();
// ...
});
// 6) Object-IDs that don't leak
// BAD: /orders/1, /orders/2 → IDORs become a numeric increment
// GOOD: /orders/ord_8f3a91... → UUIDv4 / KSUID / NanoID
// BEST: Still verify ownership server-side, regardless of how unguessable the ID looks
// 7) Mass-assignment — restrict editable fields
// BAD:
await db.users.update({ id }, req.body); // role: 'admin' lands here
// GOOD:
const { name, email } = req.body; // pluck allowed fields
await db.users.update({ id }, { name, email });
// Better: Zod / Pydantic / DTO schema
// 8) GraphQL — every resolver needs auth
const resolvers = {
Query: {
order: async (_, { id }, ctx) => {
const order = await db.orders.findOne({ id });
if (!order || order.userId !== ctx.user.id) return null;
return order;
},
},
};
// 9) Deny by default
// Every route is private unless explicitly marked public.
const PUBLIC = new Set(['/health', '/login', '/signup', '/csrf']);
app.use((req, res, next) =>
PUBLIC.has(req.path) ? next() : requireAuth(req, res, next));
// 10) Tests — write authz tests AS REGRESSIONS
describe('orders authz', () => {
it('returns 404 for orders the user doesn\'t own', async () => {
const me = await signIn('me@example.com');
const them = await createOrder('them@example.com');
const r = await me.get(`/orders/${them.id}`);
expect(r.status).toBe(404);
});
});
// 11) Audit log every privileged action
await audit.log({
actor: req.user.id,
action: 'delete_user',
target: targetUserId,
request: { ip: req.ip, ua: req.headers['user-agent'] },
at: new Date(),
});
Why it matters
Authz is the most-tested area in mature codebases — one test per route per role. Deny-by-default + scoped queries + audit logs catch the bugs that ship anyway.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// A01 Broken Access Control — the #1 risk. // Examples: IDOR (/orders/42 → other users' orders), missing role checks, // trusting client-provided role. // Fix: deny by default; enforce on the server; tests for every protected route.Try it Yourself »
Exercise
OWASP Top 10 (2021) category #1 short name.
Broken
Control
Six letters.
Discussion
Loading…