access-control-and-idor
Use when the diff adds or changes an endpoint, resolver, RPC, job or query that takes an object id, a role check, a request binding or a tenant filter - BOLA/IDOR, function-level authorization, mass assignment and tenant scoping
- License 许可
- Apache-2.0
- 最近更新
- 2026-11-15
- Stars
- ★ 122
- Forks
- 18
01 前言 Front Matter
- name
- access-control-and-idor
- source
- original; cloudflare/security-audit-skill (MIT) access-control questions (parallel paths, per-item bulk checks, body overrides), adapted; informed by trailofbits/skills (CC BY-SA 4.0, ideas only, own wording); OWASP API Security Top 10 2023, OWASP Top 10:2025 and ASVS 5.0 cited by name only
- category
- security
- tech_stack
- Web & API
- description
- Use when the diff adds or changes an endpoint, resolver, RPC, job or query that takes an object id, a role check, a request binding or a tenant filter - BOLA/IDOR, function-level authorization, mass assignment and tenant scoping
03 SKILL.md 正文
Access Control and IDOR
Overview
Broken access control is the most common serious finding in real diffs, and it rarely looks dangerous: there is no exec, no string-built query — just a lookup by id with nothing after it. You find it by asking one question of every entry point the diff touches:
"If user A sends user B's id, what stops it?"
If the answer is not a line of code you can point at, it is a finding.
1. Object-Level Authorization (BOLA / IDOR — CWE-639, CWE-862)
Enumerate every id the new code accepts: path, query, body, header, nested in a JSON object, inside a batch array, in a GraphQL argument, in a WebSocket message. Follow each to the data layer.
// ❌ any authenticated user reads any invoice
inv, err := h.repo.Get(ctx, r.PathValue("id"))
// ✅ scoped to the caller's organisation in the query itself
inv, err := h.repo.GetForOrg(ctx, r.PathValue("id"), auth.OrgID(ctx))
// ❌ Spring: findById alone, the principal is never consulted
@GetMapping("/orders/{id}") fun get(@PathVariable id: UUID) = repo.findById(id)
// ✅ ownership is part of the lookup
@GetMapping("/orders/{id}") fun get(@PathVariable id: UUID, @AuthenticationPrincipal u: User) =
repo.findByIdAndCustomerId(id, u.customerId) ?: throw NotFound()
Check the second id too: an endpoint that verifies the project in the path, then updates the task whose id is in the body, without checking the task belongs to that project.
// ❌ project ownership checked, task id from the body never tied to it
await assertProjectOwner(req.params.projectId, user.id);
await db.task.update({ where: { id: req.body.taskId }, data: { title } });
// ✅ the task is looked up inside the checked project
await db.task.update({ where: { id: req.body.taskId, projectId: req.params.projectId }, data: { title } });
2. Function-Level Authorization (BFLA — CWE-285, CWE-863)
A privileged action needs a role check, not just "logged in".
- Spring Security: a new
requestMatchers(...).permitAll()or.authenticated()on an admin path; a@PreAuthorizeremoved from a method; a check on the URL that a second mapping of the same handler bypasses. - Quarkus: a new resource method with no
@RolesAllowedwhere its class's siblings have one;@PermitAlladded. - Express/Nest/Fastify: a route registered before the auth middleware, or on a router that never mounts it.
- Django/DRF:
permission_classes = [AllowAny], a view missing@login_requiredbeside decorated siblings.
# ❌ DRF: a staff-only action reachable by any authenticated user
@action(detail=True, methods=["post"])
def refund(self, request, pk=None): ...
# ✅ the action carries the same permission as the admin viewset
@action(detail=True, methods=["post"], permission_classes=[IsAdminUser])
def refund(self, request, pk=None): ...
3. Property-Level Authorization (mass assignment — CWE-915; excessive exposure — CWE-213)
Binding a request straight onto an entity lets the client set fields it should not: role, isAdmin, ownerId, orgId, price, emailVerified.
// ❌ Mongoose: whatever the client sends becomes the document
const user = await User.create(req.body);
// ✅ an explicit allowlist of client-settable fields
const { name, email } = req.body;
const user = await User.create({ name, email });
Equivalents: Django ModelForm / DRF serializer with fields = "__all__"; Spring @ModelAttribute or @RequestBody on a JPA entity; Rails params.permit!; Pydantic/request DTO that includes role; Go json.Decode into the domain struct. The response side is the mirror: returning the entity serialises passwordHash, internal flags or other users' rows in a list.
4. Tenant Scoping
In multi-tenant code, every read and write needs the tenant predicate — including the ones people forget:
- list/search/export/count endpoints and their pagination cursors;
- background jobs and queue consumers that receive an id from a message;
- caches keyed by object id without tenant (one tenant warms, another reads);
- object storage keys built from a user-supplied filename;
- GraphQL nested resolvers (the parent was checked, the child field loads by raw id);
- batch endpoints taking
ids: [...]— each id must be checked, not the first.
5. Failing Open (CWE-280, OWASP A10:2025)
// ❌ a lookup error is treated as "no restriction"
perms, err := h.authz.For(ctx, userID)
if err == nil && !perms.CanDelete { return ErrForbidden }
return h.repo.Delete(ctx, id)
// ✅ any error denies
perms, err := h.authz.For(ctx, userID)
if err != nil || !perms.CanDelete { return ErrForbidden }
Also: a switch on role with a permissive default; a feature flag that disables the check when unset; an inverted comparison.
6. Parallel Paths to the Same Effect
The check on the route the diff adds may be right while the same state change stays reachable another way with a weaker one. For each protected effect the diff touches, list every door: the REST route and its GraphQL mutation, the v1 and v2 handlers, the single-item and the bulk endpoint, the import file, the admin tool, the background job, the WebSocket message, the agent tool. Each needs the same permission on the same resource — not authentication alone, and not a permission on a different object. Also ask whether a field in the body can override what the permission system restricts ("orgId", "ownerId", "role" in an update body) and whether bulk, export and import enforce the check per item. Tenant scoping across copies and lifecycle — caches, search, exports, soft-delete, revocation — is data-isolation-and-lifecycle.
Severity Guide
- Reading or changing another tenant's or user's data → HIGH; CRITICAL when unauthenticated, when it reaches credentials or payment data, or when it is admin takeover.
- Exposure of non-sensitive fields of another user → MEDIUM.
Common Mistakes
- Reporting "ids are guessable": UUIDs are not, and guessability is not the bug — the missing check is. A sequential id with a correct check is fine.
- Missing a check that exists one layer down: read the repository method;
GetForOwnermay already scope the query. - Reporting a missing check in client code — find the server endpoint and judge that.
- Treating 403 vs 404 as a vulnerability. Follow the repo's convention; it is not your finding.
Red Flags
WHERE id = $1with no second predicate on a caller-scoped resource.findById,get_object_or_404(Model, pk=pk),Model.findByPk(id)returning straight to the response.- A new route that its neighbours' middleware chain does not cover.
- A request DTO that is also the entity.
- An authorization helper whose error path returns
nil,trueor falls through.
References (names and links only)
OWASP API Security Top 10 2023 — API1 BOLA, API3 BOPLA, API5 BFLA · OWASP Top 10:2025 A01 Broken Access Control · ASVS 5.0 V8 Authorization
04 同仓库更多 Skills
visual-craft
暂无简介
ux-copy
Use when you write any words a user reads in a mockup or a hand-off - content language and voice, buttons, labels, errors, empty states, confirmations, numbers and dates, and the longest-string check
screen-mockup-html
Use when you draw screens as the design task's HTML mockup - ONE canvas document holding every screen and every variant side by side, the sanitizer-safe recipe, phone and wide frames, the states grid, static control states, and the loopback self-check with screenshots at 375, 768 and 1440 attached to the task
performance-awareness
How the performance score moves for your role. Use when the score context shows a declining trend, or before handing off work you have not self-checked against every criterion.
design-variants
Use when a screen design needs variants - how many, variants as side-by-side rows on the one canvas, what makes two variants genuinely different, recommending one, how the human's choice is read, and narrowing to the chosen one in revision
design-system-authoring
Use when you derive, change or extend a design system - where tokens live per stack, the DTCG token tree, role-based names for both themes, base vs layer vs drift, the DESIGN.md section order, the inventory format, the contrast check and the lint every proposal returns