/sast-idor
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
$ npx -y skills add utkusen/sast-skills --skill sast-idor --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/sast-idor
Context preview
The summary Claude sees to decide when to auto-load this skill.
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
SKILL.md
sast-idor.SKILL.mdname: sast-idor
description: >-
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.
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
# 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
# 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
// 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
// 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
# VULNERABLE
def show
@order = Order.find(params[:id])
end
# SECURE
def show
@order = current_user.orders.find(params[:id])
end
Java — Spring Boot
// 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();Read more
name: sast-idor description: >- 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.
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
# 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
# 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
// 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
// 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
# VULNERABLE def show @order = Order.find(params[:id]) end # SECURE def show @order = current_user.orders.find(params[:id]) end
Java — Spring Boot
// 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();A collection of agent skills that turn your LLM coding assistant into a fully functional SAST scanner to find vulnerabilities in your codebase. Works natively with Claude Code, Codex, Opencode, Cursor and any other assistant that supports agent skills.
Repo: utkusen/sast-skills
Other skills on sast-skills.
- /sast-analysis
Perform codebase analysis and architecture mapping as the first phase of a security assessment. Explores the tech stack, frameworks, entry points, data flows, and trust boundaries. Outputs sast/architecture.md. Run this before any vulnerability detection skill. Use when asked to
Open skill - /sast-businesslogic
Detect business logic vulnerabilities in a codebase using a three-phase approach: threat modeling (domain analysis and attack scenarios), batched verify (check exploitable gaps in parallel subagents, 3 scenarios each), and merge (consolidate batch results). Covers price
Open skill - /sast-fileupload
Detect insecure file upload vulnerabilities in a codebase using a three-phase approach: discovery (find all upload sites), batched verify (check extension bypass and related issues in parallel subagents, 3 sites each), and merge (consolidate batch results). Requires
Open skill - /sast-graphql
Detect GraphQL injection vulnerabilities in a codebase using a three-phase approach: recon (confirm GraphQL usage and find unsafe operation document assembly sites), batched verify (trace user input to those sites in parallel subagents, up to 3 candidate sites each), and merge
Open skill - /sast-hardcodedsecrets
Detect hardcoded sensitive data (API keys, access tokens, private keys, passwords, etc.) in publicly accessible code — frontend JavaScript, mobile apps, client-side bundles, and HTML templates. Uses a three-phase approach: recon (find secret candidates), batched verify (confirm
Open skill - /sast-jwt
Detect insecure JWT (JSON Web Token) implementations in a codebase using a two-phase approach: first map all JWT issuance and verification sites to understand the token lifecycle and signing configuration, then check each verification site for exploitable weaknesses such as
Open skill

