utkusen/sast-skills/sast-idor

Claude Code and agent skill by utkusen/sast-skills

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Detect Insecure Direct Object Reference (IDOR) vulnerabilities in a codebase using a three-phase approach: recon (find candidates), batched verify (check authorization in parallel subagents, 3 candidates each), and merge (consolidate batch results). Checks endpoints for missing ownership or authorization checks on user-supplied identifiers. Requires sast/architecture.md (run sast-analysis first). Outputs findings to sast/idor-results.md. Use when asked to find IDOR or authorization bypass bugs.

Install

npx vskill@latest install utkusen/sast-skills/sast-idor

SKILL.md

IDOR (Insecure Direct Object Reference) Detection

You are performing a focused security assessment to find IDOR vulnerabilities in a codebase. This skill uses a three-phase approach with subagents: recon (find candidate endpoints), batched verify (check authorization in parallel batches of 3), and merge (consolidate results).

Prerequisites: sast/architecture.md must exist. Run the analysis skill first if it doesn't.


What is IDOR

IDOR occurs when an application uses a user-supplied identifier (ID, slug, filename, etc.) to directly access an object without verifying the requesting user is authorized to access that specific object. The application authenticates the user but fails to check ownership or permissions on the requested resource.

The core pattern: authenticated user A can access or modify resources belonging to user B by changing an identifier in the request.

What IDOR IS
  • Changing /api/orders/1001 to /api/orders/1002 and seeing another user's order
  • Sending DELETE /api/documents/555 to delete a document you don't own
  • Modifying {"account_id": 789} in a request body to transfer money from someone else's account
  • Changing a file download parameter ?file_id=42 to access another user's private file
  • Updating another user's profile via PUT /api/users/other-user-id
What IDOR is NOT

Do not flag these as IDOR:

  • Missing authentication: Endpoint requires no login at all → that's "Unauthenticated Access", a different class
  • Broken function-level access control: Regular user accessing /admin/dashboard → that's vertical privilege escalation, not IDOR
  • Public resources: Accessing /api/posts/123 where posts are intentionally public is not IDOR
  • Parameter tampering on non-object fields: Changing role=admin or price=0 in a request → that's mass assignment or business logic, not IDOR
  • SQL injection via ID fields: ?id=1 OR 1=1 → that's SQLi, not IDOR
Authorization Patterns That Prevent IDOR

When you see these patterns, the endpoint is likely not vulnerable:

1. Query scoped to current user (most common fix)

# The query itself ensures only the user's own records are returned
Order.objects.filter(id=order_id, user=request.user) # Django
current_user.orders.find(params[:id]) # Rails
Order.findOne({ _id: orderId, userId: req.user.id }) # Mongoose
SELECT * FROM orders WHERE id = ? AND user_id = ? # Raw SQL

2. Explicit ownership check after fetch

order = Order.find(order_id)
if order.user_id != current_user.id:
raise Forbidden

3. Policy / ability / authorization middleware

authorize('view', order) # Laravel Policy
can?(:read, @order) # CanCanCan (Rails)
@PreAuthorize("@auth.ownsOrder(#orderId)") # Spring Security

4. Tenant/organization scoping

# Multi-tenant apps that scope all queries to the tenant
tenant = get_current_tenant(request)
Order.objects.filter(id=order_id, tenant=tenant)

Vulnerable vs. Secure Examples

Python — Django
python
# VULNERABLE: fetches any order by ID, no ownership check
def get_order(request, order_id):
order = Order.objects.get(id=order_id)
return JsonResponse(model_to_dict(order))
# SECURE: query scoped to requesting user
def get_order(request, order_id):
order = get_object_or_404(Order, id=order_id, user=request.user)
return JsonResponse(model_to_dict(order))
Python — Flask / SQLAlchemy
python
# VULNERABLE
@app.route('/api/documents/<int:doc_id>')
@login_required
def get_document(doc_id):
doc = Document.query.get_or_404(doc_id)
return jsonify(doc.serialize())
# SECURE
@app.route('/api/documents/<int:doc_id>')
@login_required
def get_document(doc_id):
doc = Document.query.filter_by(id=doc_id, owner_id=current_user.id).first_or_404()
return jsonify(doc.serialize())
Node.js — Express / Mongoose
javascript
// VULNERABLE
router.get('/api/orders/:id', auth, async (req, res) => {
const order = await Order.findById(req.params.id);
res.json(order);
});
// SECURE
router.get('/api/orders/:id', auth, async (req, res) => {
const order = await Order.findOne({ _id: req.params.id, userId: req.user.id });
if (!order) return res.status(404).json({ error: 'Not found' });
res.json(order);
});
Node.js — Express / Prisma
javascript
// VULNERABLE
router.get('/api/invoices/:id', auth, async (req, res) => {
const invoice = await prisma.invoice.findUnique({ where: { id: req.params.id } });
res.json(invoice);
});
// SECURE
router.get('/api/invoices/:id', auth, async (req, res) => {
const invoice = await prisma.invoice.findFirst({
where: { id: req.params.id, userId: req.user.id }
});
if (!invoice) return res.status(404).json({ error: 'Not found' });
res.json(invoice);
});
Ruby on Rails
ruby
# VULNERABLE
def show
@order = Order.find(params[:id])
end
# SECURE
def show
@order = current_user.orders.find(params[:id])
end
Java — Spring Boot
java
// VULNERABLE
@GetMapping("/api/accounts/{id}")
public Account getAccount(@PathVariable Long id) {
return accountRepo.findById(id).orElseThrow();
}
// SECURE
@GetMapping("/api/accounts/{id}")
public Account getAccount(@PathVariable Long id, Authentication auth) {
Account acct = accountRepo.findById(id).orElseThrow();
if (!acct.getOwnerId().equals(auth.getName()))
throw new AccessDeniedException("Forbidden");
return acct;
}
Go
go
// VULNERABLE
func GetOrder(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
order, _ := db.GetOrder(id)
json.NewEncoder(w).Encode(order)
}
// SECURE
func GetOrder(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
userID := r.Context().Value("userID").(string)
order, _ := db.GetOrderByUser(id, userID)
json.NewEncoder(w).Encode(order)
}
PHP — Laravel
php
// VULNERABLE
public function show($id) {
return Invoice::findOrFail($id);
}
// SECURE (scoped query)
public function show($id) {
return auth()->user()->invoices()->findOrFail($id);
}
// SECURE (policy)
public function show($id) {
$invoice = Invoice::findOrFail($id);
$this->authorize('view', $invoice);
return $invoice;
}
C# — ASP.NET Core
csharp
// VULNERABLE
[HttpGet("api/profiles/{id}")]
public async Task<IActionResult> GetProfile(int id) {
var profile = await _db.Profiles.FindAsync(id);
return Ok(profile);
}
// SECURE
[HttpGet("api/profiles/{id}")]
public async Task<IActionResult> GetProfile(int id) {
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var profile = await _db.Profiles.FirstOrDefaultAsync(p => p.Id == id && p.UserId == userId);
if (profile == null) return NotFound();
return Ok(profile);
}

Execution

This skill runs in three phases using subagents. Pass the contents of sast/architecture.md to all subagents as context.

Phase 1: Recon — Find Candidate Endpoints

Launch a subagent with the following instructions:

Goal: Find every endpoint, controller action, or handler that retrieves, modifies, or deletes a specific object using a user-supplied identifier. Write results to sast/idor-recon.md.
Context: You will be given the project's architecture summary. Use it to understand the tech stack, frameworks, route definitions, and data access patterns.
What to search for:
1. Route definitions that contain ID parameters:
- Path parameters: :id, {id}, <int:id>, [id]
- Search patterns: route/path/endpoint definitions with parameter placeholders
2. Controller/handler methods that accept ID arguments and use them to fetch or mutate objects:
- ORM lookups: find(id), findById(), get(id=), objects.get(), findOne(), findUnique(), findFirst(), query.get(), where(id:)
- Raw queries: SELECT ... WHERE id = ?, etc.
- Also look for delete, update operations with user-supplied IDs
3. Request body or query parameter IDs used in operations:
- req.body.userId, req.query.id, request.data['account_id'], etc.
4. GraphQL resolvers and mutations that accept ID arguments
5. File/resource access by user-supplied path or filename
What to ignore:
- Endpoints that are intentionally public (no auth required by design)
- Admin-only endpoints behind role-based checks (these are a different class)
- Endpoints where the only ID used is the authenticated user's own ID (e.g., GET /api/me/profile)
- Static asset serving
Output format — write to sast/idor-recon.md:
```markdown
# IDOR Recon: [Project Name]
## Summary
Found [N] candidate endpoints that use user-supplied identifiers to access objects.
## Candidates
### 1. [Descriptive name]
- File: path/to/file.ext (lines X-Y)
- Endpoint: METHOD /path/:param
- Identifier source: [path param / query param / body field]
- Operation: [read / update / delete]
- Object accessed: [model/table name]
- Code snippet:
```
[relevant code]
```
[Repeat for each candidate]
```
Phase 2: Verify — Check Authorization (Batched)

After Phase 1 completes, read sast/idor-recon.md and split the candidates into batches of up to 3 candidates each. Launch one subagent per batch in parallel. Each subagent verifies only its assigned candidates and writes results to its own batch file.

Batching procedure (you, the orchestrator, do this — not a subagent):

  1. Read sast/idor-recon.md and count the numbered candidate sections (### 1., ### 2., etc.).
  2. Divide them into batches of up to 3. For example, 8 candidates → 3 batches (1-3, 4-6, 7-8).
  3. For each batch, extract the full text of those candidate sections from the recon file.
  4. Launch all batch subagents in parallel, passing each one only its assigned candidates.
  5. Each subagent writes to sast/idor-batch-N.md where N is the 1-based batch number.
  6. Identify the project's primary language/framework from sast/architecture.md and select only the matching examples from the "Vulnerable vs. Secure Examples" section above. For example, if the project uses Node.js/Express with Prisma, include only the "Node.js — Express / Prisma" and "Node.js — Express / Mongoose" examples. Include these selected examples in each subagent's instructions where indicated by [TECH-STACK EXAMPLES] below.

Give each batch subagent the following instructions (substitute the batch-specific values):

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
658
Repository forks
30
7-day trend score
32

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/utkusen/sast-skills/sast-idor/badge)](https://verified-skill.com/skills/utkusen/sast-skills/sast-idor)

Something wrong with this skill or its page? Tell us.