mcpField
S
sast-idor

sast-idor

github.com/utkusen安全1.3k69 次安装5 天前更新
代码仓库

关于 sast-idor

sast-idor 是用于检测代码中不安全直接对象引用(IDOR)漏洞的安全评估 Skill。它要求先准备架构摘要,再引导 Agent 排查端点、分批验证授权并汇总结果。

主要功能

  1. 01三阶段 IDOR 评估
  2. 02发现候选端点
  3. 03分批检查授权
  4. 04汇总评估结果

适用场景

  1. 01审计通过请求 ID 访问对象的逻辑
  2. 02检查所有权与租户范围限制
  3. 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。

背景:你将获得项目的架构摘要。利用它了解技术栈、框架、路由定义和数据访问模式。

搜索内容:

  1. 包含 ID 参数的路由定义:

    • 路径参数::id、{id}、<int:id>、[id]
    • 搜索模式:带有参数占位符的路由/路径/端点定义
  2. 接受 ID 参数并使用它们来获取或修改对象的控制器/处理程序方法:

    • ORM 查询:find(id)、findById()、get(id=)、objects.get()、findOne()、findUnique()、findFirst()、query.get()、where(id:)
    • 原始查询:SELECT ... WHERE id = ? 等
    • 也要查找使用用户提供的 ID 执行的删除、更新操作
  3. 操作中使用的请求正文或查询参数 ID:

    • req.body.userId、req.query.id、request.data['account_id'] 等
  4. 接受 ID 参数的 GraphQL 解析器和变更

  5. 通过用户提供的路径或文件名访问文件/资源

忽略内容:

  • 按设计允许公开访问的端点(无需身份验证)
  • 受基于角色的检查保护、仅限管理员访问的端点(属于另一类)
  • 唯一使用的 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):

  1. 读取 sast/idor-recon.md,统计带编号的候选项章节(### 1.、### 2. 等)。
  2. 将它们划分为每批最多 3 个。例如,8 个候选项 → 3 批(1-3、4-6、7-8)。
  3. 从侦察文件中提取每批候选项章节的完整内容。
  4. 并行启动所有批次子 Agent,并且只向每个子 Agent提供分配给它的候选项。
  5. 每个子 Agent 将结果写入 sast/idor-batch-N.md,其中 N 是从 1 开始的批次编号。
  6. 从 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 的授权模式 — 如果发现以下情况,端点很可能是安全的:

  1. 查询范围限定为当前用户:查询会按已通过身份验证的用户 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))
  2. 获取对象后明确检查所有权:代码先获取对象,再在返回前比较 resource.user_id == current_user.id
  3. 策略 / 能力 / 授权中间件:框架提供的授权机制,例如 authorize('view', order)、can?(:read, @order)、@PreAuthorize("@auth.ownsOrder(#orderId)")
  4. 租户 / 组织范围限定:所有查询都限定在当前租户 / 组织范围内

适用于本项目技术栈的易受攻击与安全示例:

[TECH-STACK EXAMPLES]

对于每个候选端点,请检查:

  1. 数据库查询是否限定为已通过身份验证的用户?
    • 查询是否包含与当前用户匹配的 user_id / owner_id / tenant_id 筛选条件?
    • 查询是否通过关联完成(例如,current_user.orders.find(id))?
  2. 获取对象后是否明确检查所有权 / 权限?
    • 代码是否比较 resource.user_id == current_user.id(或等效条件)?
    • 是否进行了策略 / 能力 / 授权检查?
  3. 此路由是否应用了授权中间件?
    • 是否有中间件在处理程序运行前验证对象所有权?
    • 跟踪中间件链 — 不要想当然地认为中间件名称意味着它会检查所有权
  4. 对于变更操作(更新 / 删除),是否执行了相同的检查?
    • 有时读取端点受到保护,但写入端点没有
  5. 需要检查的边缘情况:
    • 身份验证检查是否存在,但仅在特定条件下运行(例如,对某些内容类型跳过检查)?
    • 检查是否只存在于 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。

合并步骤:

  1. 读取所有 sast/idor-batch-1.md、sast/idor-batch-2.md……文件。
  2. 收集每个批次文件中的所有发现,并合并为一个列表,同时保留原始分类和所有详细字段。
  3. 统计所有批次的总数,用于执行摘要。
  4. 按照以下格式将合并后的报告写入 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.]
  1. 写入 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 文件。

评论

支持 Markdown