Hunt Insecure Direct Object Reference (CWE-639) — missing authorization checks on object IDs. Covers horizontal vs vertical privilege escalation, UUID vs integer guessing, and GraphQL introspection-driven IDOR discovery.
64
78%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Critical
Do not install without reviewing
Fix and improve this skill with Tessl
tessl review fix ./packages/decepticon/decepticon/skills/standard/analyst/idor/SKILL.mdIDOR is the #1 source of bug bounty payouts because it's simple, ubiquitous, and scanners can't find it (authorization is business logic). Every endpoint that takes an object ID is a candidate.
/api/users/123/invoices/456?user_id=123{"orderId": 123}X-Tenant-ID, X-Customer-Idsub, tenant, roleuser(id: 123))# REST
grep -rE '@(Get|Post|Put|Delete|Patch)Mapping.*\{[a-zA-Z]+Id\}' /workspace/src # Spring
grep -rE 'router\.(get|post|put|delete)\([^)]*:id' /workspace/src # Express
grep -rE '@app\.route\([^)]*<[a-z]+:' /workspace/src # Flask
grep -rE 'resources?\s+:[a-z]+' /workspace/src # Rails
# GraphQL
grep -rE 'type Query|type Mutation' /workspace/src
grep -rE '\w+\(id: ID' /workspace/srcA "no" to question 2 = horizontal IDOR (read/write other users' data). A "no" to question 3 = vertical IDOR (regular user → admin action).
# Django: should have .filter(user=request.user) on the queryset
grep -rE 'Model\.objects\.get\(pk=' /workspace/src | grep -v 'user=request\.user'
# Rails: should have current_user.posts.find(params[:id])
grep -rE 'Post\.find\(params\[:id\]\)' /workspace/src
# Spring: should have @PreAuthorize("#id == principal.id")
grep -rE 'findById\(id\)' /workspace/src | xargs -I{} grep -L '@PreAuthorize\|@PostAuthorize'curl -X POST https://target.com/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{ __schema { types { name fields { name args { name type { name } } } } } }"}'Any query field that takes an id arg and lacks a custom directive
(@auth(requires: OWNER)) is a candidate. Mutations are even higher
value — they often forget ownership checks.
If the handler pulls user_id from request.user.id but also accepts
a user_id query param as override for "admin features", check whether
the override path enforces the admin claim. Frequently broken.
User.objects.create(**request.POST.dict())
# allows POST'ing is_staff=trueTwo-account diff:
sessionA cookie and objectA ID.sessionB cookie and objectB ID.curl -b sessionA /api/objects/<objectB> — should 403.For vertical escalation:
sessionU.curl -b sessionU -X POST /api/admin/users/<victim>/role -d '{"role":"admin"}'.GET /api/users/<victim>.Sequential integer IDs make IDOR trivial. UUIDv4 makes it harder but:
Always test UUID endpoints — the "it's a UUID so it's safe" assumption is one of the richest hunting grounds.
Negative control: same request with your own account's ID. Should return 200 with identical shape. Anything that returns 404 for your own account but 200 for someone else's is a confirmation signal.
| Variant | Vector | Score |
|---|---|---|
| Horizontal read (other user PII) | AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N | 6.5 |
| Horizontal write (modify other user) | AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N | 8.1 |
| Vertical escalation to admin | AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H | 9.9 |
| Unauth → any user profile | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N | 7.5 |
IDOR is a classic chain starter:
Weight 0.5 for authenticated IDOR, 0.3 for unauth.
0cf691e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.