关于 sast-idor
sast-idor 是用于检测代码中不安全直接对象引用(IDOR)漏洞的安全评估 Skill。它要求先准备架构摘要,再引导 Agent 排查端点、分批验证授权并汇总结果。
主要功能
- 01三阶段 IDOR 评估
- 02发现候选端点
- 03分批检查授权
- 04汇总评估结果
适用场景
- 01审计通过请求 ID 访问对象的逻辑
- 02检查所有权与租户范围限制
- 03审查修改或删除对象的端点
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ utkusen/ sast-skills --skill sast-idor安装后在 Claude Code 中输入/sast-idor确认已加载。
详细介绍
IDOR(不安全的直接对象引用)检测
你正在对代码库进行有针对性的安全评估,以查找 IDOR 漏洞。此技能采用由子 Agent 执行的三阶段方法:侦察(查找候选端点)、批量验证(每批并行检查 3 个端点的授权情况)和合并(汇总结果)。
前置条件:必须存在 sast/architecture.md。如果不存在,请先运行分析技能。
什么是 IDOR
当应用使用用户提供的标识符(ID、slug、文件名等)直接访问对象,却未验证请求用户是否有权访问该特定对象时,就会发生 IDOR。应用对用户进行了身份验证,却未检查用户是否拥有所请求的资源或是否具备相应权限。
核心模式:已通过身份验证的用户 A 可以通过更改请求中的标识符,访问或修改属于用户 B 的资源。
什么情况属于 IDOR
- 将
/api/orders/1001改为/api/orders/1002,从而看到其他用户的订单 - 发送
DELETE /api/documents/555删除不属于自己的文档 - 在请求正文中修改
{"account_id": 789},从而从他人账户转出资金 - 将文件下载参数
?file_id=42改为其他值,从而访问其他用户的私有文件 - 通过
PUT /api/users/other-user-id更新其他用户的个人资料
什么情况不属于 IDOR
请勿将以下情况标记为 IDOR:
- 缺少身份验证:端点完全不需要登录 → 这是“未认证访问”,属于另一类问题
- 功能级访问控制失效:普通用户访问
/admin/dashboard→ 这是垂直权限提升,不是 IDOR - 公开资源:访问
/api/posts/123,而帖子本身有意公开 → 不属于 IDOR - 篡改非对象字段参数:在请求中将
role=admin或price=0改为其他值 → 这是批量赋值或业务逻辑问题,不是 IDOR - 通过 ID 字段进行 SQL 注入:
?id=1 OR 1=1→ 这是 SQLi,不是 IDOR
可防止 IDOR 的授权模式
如果看到以下模式,端点通常不存在漏洞:
1. 查询限定为当前用户(最常见的修复方式)
# 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. 获取对象后显式检查所有权
order = Order.find(order_id)
if order.user_id != current_user.id:
raise Forbidden
3. 策略/能力/授权中间件
authorize('view', order) # Laravel Policy
can?(:read, @order) # CanCanCan (Rails)
@PreAuthorize("@auth.ownsOrder(#orderId)") # Spring Security
4. 租户/组织范围限定
# Multi-tenant apps that scope all queries to the tenant
tenant = get_current_tenant(request)
Order.objects.filter(id=order_id, tenant=tenant)
易受攻击与安全示例
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();
if (!acct.getOwnerId().equals(auth.getName()))
throw new AccessDeniedException("Forbidden");
return acct;
}
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
// 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
// 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);
}
执行
此技能通过子 Agent 分三个阶段运行。将 sast/architecture.md 的内容作为上下文传递给所有子 Agent。
阶段 1:侦察 — 查找候选端点
启动一个子 Agent,并提供以下说明:
目标:查找每个使用用户提供的标识符来检索、修改或删除特定对象的端点、控制器操作或处理程序。将结果写入
sast/idor-recon.md。背景:你将获得项目的架构摘要。利用它了解技术栈、框架、路由定义和数据访问模式。
搜索内容:
包含 ID 参数的路由定义:
- 路径参数:
:id、{id}、<int:id>、[id]- 搜索模式:带有参数占位符的路由/路径/端点定义
接受 ID 参数并使用它们来获取或修改对象的控制器/处理程序方法:
- ORM 查询:
find(id)、findById()、get(id=)、objects.get()、findOne()、findUnique()、findFirst()、query.get()、where(id:)- 原始查询:
SELECT ... WHERE id = ?等- 也要查找使用用户提供的 ID 执行的删除、更新操作
操作中使用的请求正文或查询参数 ID:
req.body.userId、req.query.id、request.data['account_id']等接受 ID 参数的 GraphQL 解析器和变更
通过用户提供的路径或文件名访问文件/资源
忽略内容:
- 按设计允许公开访问的端点(无需身份验证)
- 受基于角色的检查保护、仅限管理员访问的端点(属于另一类)
- 唯一使用的 ID 是已通过身份验证的用户自己的 ID 的端点(例如
GET /api/me/profile)- 静态资源服务
输出格式 — 写入
sast/idor-recon.md:# IDOR 侦察:[项目名称] ## 摘要 找到 [N] 个使用用户提供的标识符访问对象的候选端点。 ## 候选项 ### 1. [描述性名称] - **文件**:`path/to/file.ext`(第 X-Y 行) - **端点**:`METHOD /path/:param` - **标识符来源**:[路径参数/查询参数/正文中的字段] - **操作**:[读取/更新/删除] - **访问的对象**:[模型/表名称] - **代码片段**:[相关代码]
[每个候选项重复以上格式]
阶段 2:验证 — 检查授权(分批)
阶段 1 完成后,读取 sast/idor-recon.md,并将候选项拆分为每批最多 3 个候选项。并行启动每批一个子 Agent。每个子 Agent 仅验证分配给自己的候选项,并将结果写入自己的批次文件。
分批流程(由你这个协调者执行,不要交给子 Agent):
- 读取
sast/idor-recon.md,统计带编号的候选项章节(### 1.、### 2. 等)。 - 将它们划分为每批最多 3 个。例如,8 个候选项 → 3 批(1-3、4-6、7-8)。
- 从侦察文件中提取每批候选项章节的完整内容。
- 并行启动所有批次子 Agent,并且只向每个子 Agent提供分配给它的候选项。
- 每个子 Agent 将结果写入
sast/idor-batch-N.md,其中 N 是从 1 开始的批次编号。 - 从
sast/architecture.md确定项目的主要语言/框架,并且仅选择上方“易受攻击与安全示例”部分中匹配的示例。例如,如果项目使用 Node.js/Express 和 Prisma,则仅包含“Node.js — Express / Prisma”和“Node.js — Express / Mongoose”示例。在每个子 Agent 的说明中,按下方[TECH-STACK EXAMPLES]所示加入所选示例。
向每个批次子 Agent 提供以下说明(替换为该批次对应的值):
目标:验证以下 IDOR(不安全的直接对象引用)候选项,并确定是否存在充分的授权检查。我们的目标是查找 IDOR 漏洞。将结果写入
sast/idor-batch-[N].md。分配给你的候选项(来自侦察阶段):
[在此粘贴分配给你的候选项章节全文,并保留原始编号]
背景:你将获得项目的架构摘要。请利用它了解身份验证机制、中间件栈和 ORM 模式。
IDOR 参考 — 检查要点:
当已通过身份验证的用户通过更改请求中的标识符,访问或修改属于其他用户的资源时,就会发生 IDOR。重点关注水平权限提升(用户对用户)。
以下情况不属于 IDOR — 请勿将其标记为 IDOR:
- 缺少身份验证:端点完全不需要登录 → 这是“未通过身份验证的访问”,不是 IDOR
- 功能级访问控制失效:普通用户访问
/admin/dashboard→ 这是垂直权限提升,不是 IDOR- 公开资源:访问
/api/posts/123,而这些帖子本来就是公开的,这不属于 IDOR- 篡改非对象字段的参数:更改
role=admin或price=0→ 这是批量赋值或业务逻辑问题,不是 IDOR- 通过 ID 字段进行 SQL 注入:
?id=1 OR 1=1→ 这是 SQLi,不是 IDOR可防止 IDOR 的授权模式 — 如果发现以下情况,端点很可能是安全的:
- 查询范围限定为当前用户:查询会按已通过身份验证的用户 ID 进行筛选(例如,
WHERE id = ? AND user_id = ?、current_user.orders.find(id)、Order.findOne({ _id: id, userId: req.user.id })、Order.objects.filter(id=order_id, user=request.user))- 获取对象后明确检查所有权:代码先获取对象,再在返回前比较
resource.user_id == current_user.id- 策略 / 能力 / 授权中间件:框架提供的授权机制,例如
authorize('view', order)、can?(:read, @order)、@PreAuthorize("@auth.ownsOrder(#orderId)")- 租户 / 组织范围限定:所有查询都限定在当前租户 / 组织范围内
适用于本项目技术栈的易受攻击与安全示例:
[TECH-STACK EXAMPLES]
对于每个候选端点,请检查:
- 数据库查询是否限定为已通过身份验证的用户?
- 查询是否包含与当前用户匹配的
user_id/owner_id/tenant_id筛选条件?- 查询是否通过关联完成(例如,
current_user.orders.find(id))?- 获取对象后是否明确检查所有权 / 权限?
- 代码是否比较
resource.user_id == current_user.id(或等效条件)?- 是否进行了策略 / 能力 / 授权检查?
- 此路由是否应用了授权中间件?
- 是否有中间件在处理程序运行前验证对象所有权?
- 跟踪中间件链 — 不要想当然地认为中间件名称意味着它会检查所有权
- 对于变更操作(更新 / 删除),是否执行了相同的检查?
- 有时读取端点受到保护,但写入端点没有
- 需要检查的边缘情况:
- 身份验证检查是否存在,但仅在特定条件下运行(例如,对某些内容类型跳过检查)?
- 检查是否只存在于
if/else的一个分支,另一个分支却缺失?- 是否可以通过在其他字段中发送 ID 来绕过检查?
- 批量 / 批处理端点是逐项检查,还是只在批次级别检查?
分类:
- 易受攻击:未发现针对特定对象的授权检查。用户 A 可以访问用户 B 的资源。
- 可能易受攻击:存在身份验证检查,但看起来不完整、可绕过或有条件限制。
- 不易受攻击:已实施适当的授权检查。
- 需要人工审核:无法有把握地确定(例如,中间件链复杂,或授权发生在难以追踪的服务层中)。
输出格式 — 写入
sast/idor-batch-[N].md:# IDOR Batch [N] Results ## Findings ### [VULNERABLE] Endpoint name - **File**: `path/to/file.ext` (lines X-Y) - **Endpoint**: `METHOD /path/:param` - **Issue**: [清楚说明缺少了什么] - **Impact**: [攻击者可以执行的操作 — 例如读取其他用户的 X、删除其他用户的 Y 等] - **Proof**: [展示从路由到数据库查询的代码路径,并指出缺失的检查] - **Remediation**: [针对此端点的具体修复方法] - **Dynamic Test**:[用于在运行中的应用上确认此问题的 curl 命令或分步说明。 包含确切的端点、HTTP 方法、标头,以及需要在响应中查看的内容。 使用 <USER_B_TOKEN> 和 <USER_A_RESOURCE_ID> 等占位令牌。]
### [LIKELY VULNERABLE] Endpoint name - **File**: `path/to/file.ext` (lines X-Y) - **Endpoint**: `METHOD /path/:param` - **Issue**: [说明检查不完整之处] - **Concern**: [说明此问题为何仍可能被利用] - **Proof**: [展示包含薄弱 / 部分检查的代码路径] - **Remediation**: [具体修复方法] - **Dynamic Test**:[用于在运行中的应用上确认此问题的 curl 命令或分步说明。 包含确切的端点、HTTP 方法、标头,以及需要在响应中查看的内容。 使用 <USER_B_TOKEN> 和 <USER_A_RESOURCE_ID> 等占位令牌。]
### [NOT VULNERABLE] Endpoint name - **File**: `path/to/file.ext` (lines X-Y) - **Endpoint**: `METHOD /path/:param` - **Protection**: [说明保护方式 — 范围限定查询 / 所有权检查 / 策略] ### [NEEDS MANUAL REVIEW] Endpoint name - **File**: `path/to/file.ext` (lines X-Y) - **Endpoint**: `METHOD /path/:param` - **Uncertainty**: [说明自动化分析无法确定状态的原因] - **Suggestion**: [说明需要人工检查的内容]
阶段 3:合并 — 汇总批次结果
所有阶段 2 批次子 Agent 完成后,读取每个 sast/idor-batch-*.md 文件,并将其合并为一个 sast/idor-results.md。由你(编排者)直接完成此操作——无需子 Agent。
合并步骤:
- 读取所有
sast/idor-batch-1.md、sast/idor-batch-2.md……文件。 - 收集每个批次文件中的所有发现,并合并为一个列表,同时保留原始分类和所有详细字段。
- 统计所有批次的总数,用于执行摘要。
- 按照以下格式将合并后的报告写入
sast/idor-results.md:
# IDOR Analysis Results: [Project Name]
## Executive Summary
- Candidates analyzed: [total across all batches]
- Vulnerable: [N]
- Likely Vulnerable: [N]
- Not Vulnerable: [N]
- Needs Manual Review: [N]
## Findings
[All findings from all batches, grouped by classification:
VULNERABLE first, then LIKELY VULNERABLE, then NEEDS MANUAL REVIEW, then NOT VULNERABLE.
Preserve every field from the batch results exactly as written.]
- 写入
sast/idor-results.md后,删除所有中间批次文件(sast/idor-batch-*.md)。
重要提醒
- 读取
sast/architecture.md,并将其内容作为上下文提供给所有子 Agent。 - 阶段 2 必须在阶段 1 完成后运行——它依赖侦察结果。
- 阶段 3 必须在所有阶段 2 批次完成后运行——它依赖所有批次的输出。
- 每个子 Agent 的批次大小为 3 个候选项。如果候选项总数为 1–3 个,则使用单个子 Agent。如果有 10 个,则使用 4 个子 Agent(3+3+3+1)。
- 并行启动所有批次子 Agent——不要按顺序逐个运行。
- 每个批次子 Agent 只接收侦察文件中分配给它的候选项文本,而不是整个侦察文件。这样可以让每个子 Agent 的上下文保持精简且聚焦。
- 聚焦于水平权限提升(用户到用户)。垂直权限提升(用户到管理员)属于另一项技能。
- 如有疑问,归类为“需要人工审查”,而不是“无漏洞”。在安全评估中,漏报比误报更糟糕。
- 追踪完整的代码路径:路由 → 中间件 → 控制器 → 服务 → 数据访问。授权检查可能发生在任何一层。
- 注意框架惯例。在 Rails 中,
current_user.orders.find(id)是安全的。在 Express 中,仅有auth中间件并不意味着已检查所有权。 - 清理中间文件:写入最终的
sast/idor-results.md后,删除sast/idor-recon.md和所有sast/idor-batch-*.md文件。