broken-access-control
Broken Access Control
Auditing and fixing authorization gaps — ensuring every route checks who can access which specific resource, not just whether the caller is logged in.
The Core Distinction
Authentication → "Are you logged in?" (identity)
Authorization → "Can you access THIS record?" (permission)
Most auth middleware only handles the first. The second must be enforced per-endpoint, per-resource.
Vulnerability Patterns
1. IDOR (Insecure Direct Object Reference)
User supplies an ID; server fetches it without checking ownership.
// ❌ Authenticated but not authorized
app.get('/invoices/:id', requireAuth, async (req, res) => {
const invoice = await db.invoices.findById(req.params.id);
res.json(invoice); // any logged-in user gets any invoice
});
// ✅ Ownership check
app.get('/invoices/:id', requireAuth, async (req, res) => {
const invoice = await db.invoices.findOne({
id: req.params.id,
owner_id: req.user.id // scope to caller
});
if (!invoice) return res.status(404).send(); // 404, not 403 — don't confirm existence
});
Rule: Never fetch by ID alone. Always include owner_id = req.user.id (or equivalent) in the query.
2. Missing Role Check
Endpoint exists, auth required, but no role/permission gate.
// ❌ Any authenticated user can delete users
app.delete('/admin/users/:id', requireAuth, deleteUser);
// ✅
app.delete('/admin/users/:id', requireAuth, requireRole('admin'), deleteUser);
3. Privilege Escalation via User-Supplied Role
User sends their own role in the request body and it gets written to the DB.
// ❌
app.put('/users/:id', requireAuth, async (req, res) => {
await db.users.update(req.params.id, req.body); // role, plan, credits — all writable
});
// ✅ Explicit field allowlist
app.put('/users/:id', requireAuth, async (req, res) => {
const { name, email, bio } = req.body; // only safe fields
await db.users.update(req.params.id, { name, email, bio });
});
4. Bypassing via HTTP Method
Authorization check tied to a specific method; same route accessible via another.
// ❌ Only GET is gated; POST to same path skips the check
router.get('/reports/:id', requireAuth, getReport);
router.post('/reports/:id', processReport); // forgot auth here
Check every verb on a route independently.
5. Path Traversal / Parameter Manipulation
ID in URL vs. ID in JWT disagree; server trusts the URL.
// ❌ User can PUT /users/999 even if their token says id=1
app.put('/users/:id', requireAuth, async (req, res) => {
await db.users.update(req.params.id, req.body);
});
// ✅ Ignore URL param for self-edit; use token
app.put('/users/me', requireAuth, async (req, res) => {
await db.users.update(req.user.id, sanitize(req.body));
});
6. Nested Resource Without Parent Check
Child resource ownership is checked, but parent isn't.
// ❌ Checks comment ownership but not post ownership
app.delete('/posts/:postId/comments/:commentId', requireAuth, async (req, res) => {
const comment = await db.comments.findOne({ id: req.params.commentId, author_id: req.user.id });
if (!comment) return res.status(404).send();
await comment.delete();
});
// ✅ Verify the post belongs to the user too (if that's the intent)
const post = await db.posts.findOne({ id: req.params.postId, owner_id: req.user.id });
if (!post) return res.status(404).send();
7. Tenant Isolation (Multi-Tenant Apps)
Query scoped to user but not to tenant — user from Org A can read Org B's data.
// ❌ Scoped to user but not tenant
const records = await db.records.findAll({ user_id: req.user.id });
// ✅ Always include tenant scope
const records = await db.records.findAll({
user_id: req.user.id,
tenant_id: req.user.tenant_id // from token, never from request body
});
Authorization Models
Choose one and apply it consistently. See references/models.md for implementation detail.
| Model | Best for | Key concept |
|---|---|---|
| RBAC (Role-Based) | Clear user roles (admin, editor, viewer) | Role → permission set |
| ABAC (Attribute-Based) | Fine-grained, context-sensitive | Policy rules on resource + user attributes |
| Ownership | User-owned resources | resource.owner_id === req.user.id |
| ReBAC (Relation-Based) | Hierarchical resources, sharing | Graph of relationships (e.g. Google Zanzibar) |
Most apps need Ownership + RBAC together: ownership for user data, roles for admin surfaces.
Audit Process
Step 1 — Map Every Route
List all endpoints with their HTTP verb. Flag any with no requireAuth or equivalent middleware.
Step 2 — Classify Each Route
For each authenticated route, determine what check is needed:
| Route type | Required check |
|---|---|
| User's own resource | resource.owner_id === req.user.id |
| Admin operation | req.user.role === 'admin' (or permission bit) |
| Shared/org resource | resource.org_id === req.user.org_id |
| Public read | No auth needed — confirm intentional |
Step 3 — Check the Query
For every DB fetch: does the WHERE clause include the ownership/tenant scope, or does it fetch by ID alone then check in application code?
// Risky — fetch then check (TOCTOU, extra DB round-trip, easy to forget)
const item = await db.findById(id);
if (item.owner !== req.user.id) return res.status(403);
// Better — let the DB enforce it
const item = await db.findOne({ id, owner_id: req.user.id });
if (!item) return res.status(404);
Step 4 — Check Write Paths
Every POST / PUT / PATCH / DELETE: are fields allowlisted? Can a user escalate their own role, balance, or plan via this endpoint?
Step 5 — Check Indirect References
Are there endpoints that accept email, username, or slug instead of ID? Apply the same ownership check — just via a different lookup key.
Response Codes
| Situation | Code | Reason |
|---|---|---|
| Not logged in | 401 | Client should re-authenticate |
| Logged in, wrong permissions | 403 | Client is authenticated but not authorized |
| Resource exists but caller shouldn't know | 404 | Don't leak existence of records |
Use 404 (not 403) when confirming a record's existence would itself be a data leak (e.g., other users' private invoices).
Audit Checklist
- Every route has
requireAuth(or is explicitly marked public) - Every resource fetch includes ownership/tenant scope in the query — not just auth check
- Admin/privileged routes have role/permission checks beyond just
requireAuth - Write endpoints allowlist accepted fields — no
req.bodypassed directly to ORM - No user-supplied
role,plan,credits, or similar fields accepted on update - Nested resources: parent ownership verified before child access
- Multi-tenant:
tenant_idalways in queries, sourced from token not request - All HTTP verbs on a route are gated, not just GET
- URL params not trusted for self-referencing operations (use
req.user.id) - 403 vs 404 used correctly — sensitive resource existence not leaked
Reference Files
references/models.md— RBAC, ABAC, ownership middleware patterns with codereferences/testing.md— How to test for access control bugs (unit, integration, manual)
More from blunotech-dev/agents
anti-purple-ui
Enforce a strict monochrome UI with a single high-contrast accent color, removing generic tech gradients and “AI-style” palettes. Use when the user wants minimal, anti-AI, or non-generic aesthetics, or says the UI looks too techy or generic.
9harmonize-whitespace
Align all spacing (padding, margins, gaps) to a consistent 4pt/8pt grid. Use when spacing feels off, inconsistent, cramped, or unbalanced, or when the user asks for a spacing scale or alignment fix.
9typographic-hierarchy
Improve typography by adjusting font sizes, weights, spacing, and contrast to create clear visual hierarchy and readability. Use when text feels flat, unstructured, or when the user asks to refine headings, type scale, or overall readability.
6micro-interaction-adder
Add polished CSS micro-interactions like hover effects, transitions, and feedback states to improve UI feel. Use when the user asks for animations, better UX, or when the interface feels static, plain, or unresponsive.
4consistent-border-radius
Normalizes rounded corners across a file so buttons, inputs, cards, modals, badges, and all UI elements share the exact same curvature. Use this skill whenever the user mentions inconsistent border radii, wants to unify rounded corners, asks to make UI elements look more cohesive, or says things like "make the corners match", "fix the rounding", "unify border radius", "standardize my rounded corners", or "buttons and cards don't match". Also trigger when the user pastes a CSS/HTML/JSX/TSX file and asks for a design consistency pass, border radius is one of the first things to normalize.
4component-split
Analyze a component and determine when and how to split it based on size, responsibility, and reuse signals, producing a refactored structure with clear boundaries. Use when users share large, mixed-concern, or hard-to-test components, or ask about splitting, refactoring, or improving component architecture.
3