mcpField
A
authz-security

authz-security

github.com/superagent-ai安全7972 次安装11 周前更新
代码仓库

关于 authz-security

authz-security 是一项由模型辅助的代码审查技能,用于发现路由、控制器、解析器和数据访问中的授权检查缺失。它先梳理资源归属与身份,再按严重程度报告问题并提供适配框架的修复方案;无需运行应用或提供凭据。

主要功能

  1. 01梳理资源归属和租户模型
  2. 02审查路由与对象访问
  3. 03检测对象级和功能级授权缺口
  4. 04检查批量赋值与客户端身份
  5. 05比较不同端点的授权覆盖
  6. 06报告严重程度和代码修复方案

适用场景

  1. 01检查 API 处理程序中的 IDOR 或 BOLA
  2. 02确认管理端点是否校验角色权限
  3. 03评估集合查询的租户范围限制
  4. 04审查代码差异中的授权变更

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/superagent-ai/skills --skill authz-security

安装后在 Claude Code 中输入/authz-security确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
checklist.md
frameworks.md
patterns.md

详细介绍

授权安全扫描器

此技能可将模型转变为授权审查员。阅读路由、控制器、解析器和数据模型;逐步执行检测流程;报告每一处缺失的访问控制检查,并给出严重级别和符合框架规范的具体修复方案。无需安装工具、运行应用或提供凭据——分析过程就是模型阅读代码。

访问控制失效是 OWASP Top 10 中排名第 1 的 Web 风险(A01),也是 API 风险中的前两大风险(API1 BOLA、API5 BFLA)。它之所以总是高居榜单前列,恰恰是因为自动化工具会漏掉它:没有可供 grep 查找的危险函数调用。漏洞在于检查的缺失,而确认这一点需要理解应用的所有权模型——这正是阅读代码的模型能够做到、签名扫描器却做不到的事。

思维模型

身份验证回答的是*你是谁?授权回答的是你是否有权对这个特定对象执行此操作?*此技能只关注第二个问题。处理程序即使完成了完善的身份验证,仍可能存在严重漏洞。

每项特权操作都可能沿两个方向被越权:

  • 水平越权 — 以同一权限级别访问其他用户的对象。篡改对象标识符(/orders/1042 → /orders/1043),就能读取他人的数据。这就是 BOLA / IDOR(OWASP API1)。
  • 垂直越权 — 访问权限级别高于自身的功能。普通用户访问管理员端点。这就是 BFLA(OWASP API5)。

缺陷几乎总是缺少代码,而非代码错误:查询按对象 ID 过滤,却不按所有者过滤;管理员路由只检查“是否已登录”,却不检查“是否为管理员”;请求体直接绑定到模型。因此,你要扫描的是一个缺口,而不是某种模式。

**当你无法判断检查是否存在时,就假定它不存在,并标记出来以供确认。**误报的代价是一条审查意见。漏报的代价则是所有用户的数据。以下每项判断都应遵循这种不对称性。

扫描流程

按顺序执行以下流程。流程 0 会构建其余流程所依赖的上下文——不要跳过。

流程 0:梳理所有权和租户模型

只有先了解在此处“拥有”对象意味着什么,才能判断检查是否缺失。在阅读任何端点之前,先阅读数据层——ORM 模型、架构、迁移——并确认:

  • 每种资源由谁拥有? 查找所有者或范围列:user_id、account_id、org_id、tenant_id、team_id、workspace_id。带有此类列的资源,在限定到相应范围之前,绝不能返回或修改。
  • 主体是什么? 已验证身份来自哪里——会话中的 request.user、由身份验证中间件设置的 req.user,还是经过验证的 JWT sub 声明?这是唯一可信的身份。请记录下来。
  • 采用什么租户策略? 按行隔离(使用带有 tenant_id 列的共享表)是几乎所有多租户 BOLA 的藏身之处,因为隔离取决于每条查询都记得添加过滤条件。按租户划分架构或数据库会将边界下沉到更底层,因此更不容易出错。

记录这份映射(即使只是非正式记录也可以)。之后的每项发现都可以表述为“主体 P 接触了对象 O,却未确认 P 是否有权访问 O”——你需要知道 O 的所有者,才能作出判断。

流程 1:枚举处理程序及其访问的对象

列出每个入口:REST 路由、控制器操作、GraphQL 解析器、RPC/gRPC 方法、服务端操作。对每个入口,记录:

  • HTTP 方法 + 路径(或解析器名称)。
  • 读取或修改的对象。
  • 对象标识符的来源:路径参数、查询字符串、请求体或会话/令牌。

标记每个通过客户端提供的标识符加载或修改对象的处理程序——这些就是流程 2 中的 BOLA 候选项。只使用会话中的主体自身 ID(GET /me、current_user.orders)的处理程序风险较低,可以降低优先级。

第 2 轮:对象级授权(BOLA / IDOR)— OWASP API1

对于第 1 轮标记的每个处理程序,请检查:是否有检查确认主体拥有或有权访问这个特定对象? 可接受的检查有以下三种形式之一:

  1. 按所有者限定范围的查询 — 查找操作本身包含所有者条件,因此找不到不属于该用户的 id:
    invoice = Invoice.objects.get(id=id, org=request.user.org)   # 404 on someone else's
    
  2. 加载后显式检查 — 先加载,再与主体进行比较并拒绝:
    invoice = Invoice.objects.get(id=id)
    if invoice.org_id != request.user.org_id:
        raise PermissionDenied
    
  3. 集中式策略 / 权限 — 编码了规则的防护机制:authorize! :read, invoice(Pundit/CanCanCan)、@PreAuthorize("hasPermission(#id,...)")、OPA/Cedar/OpenFGA 决策或策略关卡。

如果对象是通过客户端提供的 id 加载的,且没有上述任何检查——仅使用 id,并假定“你必须先登录才能访问这里”——这就是典型的 IDOR。身份验证不等于授权。

// [P0] BOLA — Express: looks up by id, never checks ownership
app.get("/api/orders/:id", requireAuth, async (req, res) => {
  const order = await Order.findByPk(req.params.id);
  res.json(order);
});

// Fixed — scope to the authenticated principal
app.get("/api/orders/:id", requireAuth, async (req, res) => {
  const order = await Order.findOne({
    where: { id: req.params.id, userId: req.user.id },
  });
  if (!order) return res.sendStatus(404);
  res.json(order);
});

严重性:对于敏感数据读取,或任何可由任意已验证用户执行的写入 / 删除,评为 P0;如果 id 空间很大且难以猜测,并且数据敏感度低,则评为 P1(仍需报告问题——难以猜测的 id 也会泄露)。

第 3 轮:功能级授权(BFLA / 垂直越权)— OWASP API5

执行特权操作的端点——用户管理、角色变更、管理控制面板、数据导出、计费、内部工具、功能标志——必须检查主体的角色或权限,而不只是检查主体是否已通过身份验证。

标记:

  • /admin/* 路由或特权操作仅由身份验证中间件保护,而处理程序或其中间件链中没有角色 / 权限断言。
  • 角色检查只在前端执行(隐藏按钮),而 API 端点仍可直接访问。
  • 依赖隐蔽性保护的“隐藏”管理端点——未公开文档且没有防护的路径。
# [P1] BFLA — authenticated, but any user can promote anyone to admin
@router.post("/users/{id}/role")
def set_role(id: int, body: RoleIn, user=Depends(current_user)):
    User.objects.filter(id=id).update(role=body.role)

# Fixed — assert the privilege
@router.post("/users/{id}/role")
def set_role(id: int, body: RoleIn, user=Depends(current_user)):
    if not user.is_admin:
        raise HTTPException(403)
    User.objects.filter(id=id).update(role=body.role)
第 4 轮:批量赋值 / 过度属性绑定 — OWASP API3

当处理程序将整个请求体绑定到模型时——User.update(req.body)、Model(**request.data)、user.update_attributes(params[:user])、Object.assign(entity, req.body)——请检查客户端不应控制哪些字段:role、is_admin、is_verified、email_verified、owner_id、org_id、tenant_id、balance、price、status。如果客户端能控制其中任何字段,就可以通过添加字段提升权限,或将对象移至其他租户。

修复方法:明确允许写入的字段——使用强参数、包含命名字段的序列化器 / DTO,或仅用于输入的 Pydantic 模型。切勿将原始请求体绑定到持久化模型。

第 5 轮:信任客户端提供的身份信息

主体和租户必须在服务端从会话或经过验证的令牌中派生,绝不能来自客户端可控的值。标记以下用于授权决策或查询范围限定的值:

  • 从请求体、查询字符串、路径或自定义标头(X-User-Id、X-Tenant)读取的 user_id / account_id / org_id。
  • 未验证签名、签发者、受众和有效期就信任的 JWT 声明。
// [P0] — scopes by a query param the caller controls
app.get("/api/invoices", requireAuth, async (req, res) => {
  res.json(await Invoice.findAll({ where: { userId: req.query.userId } }));
});
// Fixed: scope by the session principal, ignore client-supplied identity
//   where: { userId: req.user.id }
第 6 轮:一致性与边缘场景

授权漏洞常见于覆盖不均之处。逐项检查:

  • 同级操作。 GET /orders/:id 会检查所有权,但同一资源上的 PUT/PATCH/DELETE 不会(或反过来)。请验证每种方法,而不只是读取操作。
  • 嵌套资源。 /orgs/:orgId/projects/:projectId —— 是否验证了父级与子级的关联(项目是否确实属于该组织,用户是否属于该组织)?常见漏洞是只检查叶节点,并信任路径前缀。
  • 批量端点。 这些端点接收 ID 数组——必须对每个元素执行检查,而不是只检查第一个。GraphQL 别名查询也一样。
  • 列表/集合端点。 GET /invoices 没有可替换的 ID,因此很容易忘记按租户限制范围。未限定范围的列表会泄露整张表——其严重程度往往高于单个对象的 IDOR,属于 P0。
  • GraphQL。 node(id:)/全局对象标识,以及每个加载关联对象的字段解析器,都必须在各自的解析器中执行授权;只检查查询根节点并不足够。
  • 差异中新增的端点。 v2 路由、新增的控制器操作、新字段解析器——这些恰恰是最容易忘记加防护的地方。将差异中任何新增的按 ID 获取对象的处理器自动纳入第 2 轮审查。
第 7 轮:间接措施或非控制措施

这些措施看似与授权有关,实则不然。当它们是用户之间唯一的防护时,严重程度与它们所替代的缺失检查相同:

  • 中间件/路由顺序。 依赖注册顺序确保防护先运行——之后只要在它之前添加路由,或者框架先匹配更具体的路由,就会轻易失效。
  • 仅在前端执行限制。 隐藏按钮、保护 SPA 路由,但其背后的 API 仍然开放。
  • 机密标识符。 将 UUID 或 slug 作为唯一控制措施。ID 会通过日志、Referer 标头、错误消息和 API 响应泄露,而且 UUIDv1 部分可预测。难以猜测的 ID 只能作为纵深防御,不能替代检查(如果其他授权措施正确,则为 P3)。
  • 默认允许配置。 框架默认在没有匹配规则时允许访问:缺少 permission_classes、Spring permitAll()、兜底路由、策略引擎虽被查询但默认决策为允许。

发现项格式

按以下结构报告每项发现。按严重程度分组,P0 优先。

[P0] bola in app/controllers/orders_controller.rb:42
  #show loads Order by params[:id] with no ownership check. Any authenticated
  user can read any order by changing the id (sequential integers).
  Principal: current_user. Order belongs to a user via user_id.

  Vulnerable:
    def show
      @order = Order.find(params[:id])
    end

  Fix (scope the lookup to the owner; 404s on non-owned ids):
    def show
      @order = current_user.orders.find(params[:id])
    end

如果用户粘贴的代码片段没有文件路径,请使用“该处理器”指代,并使用代码片段内的行号。务必写明对象、对象所有者和主体——这三者可以证明检查缺失。

严重程度等级

  • P0——任何已认证用户(通常甚至是任何未认证用户)现在都能利用,且无需组合利用:敏感读取或任何写入/删除操作缺少对象级检查、普通用户可访问管理员功能、未限定范围的集合端点,或依据客户端提供的身份进行范围限制。阻止合并。
  • P1——不太敏感的操作缺少角色检查、特权/租户字段可被批量赋值,或 BOLA 仅受庞大且难以猜测的 ID 空间保护。凡涉及其他用户数据的情况都应阻止合并。
  • P2——覆盖不均,但尚无法利用:某个操作有范围限制而同级操作情况不明确、存在但脆弱的租户范围限制,或依赖无法通过静态分析确认的中间件的检查。
  • P3——纵深防御:其他授权措施正确,但 ID 可预测;缺少授权回归测试;将 ID 保密作为辅助防护层。

当代码看起来没问题时

完成所有轮次后,如果没有发现问题:

  1. 明确说明这一点,并指出已验证的内容:第 0 轮检查的所有权模型、已枚举的处理程序,以及所有对象 ID 路径和特权函数在所有 HTTP 动词下都进行了检查。
  2. 说明静态审查无法确认的内容:运行时中间件的接线和顺序、未查看的集中式策略的实际内容、框架默认行为,以及部署配置是否与代码一致。
  3. 建议针对运行中的实例执行动态 BOLA 测试(使用两个账户,交换 ID),并检查 references/checklist.md,以此进行确认。这项技能不会执行这类互补的攻防检查。

静态审查未发现问题,并不能证明部署是安全的。

参考文件

  • references/frameworks.md — 各主要技术栈中正确的所有权检查和角色检查应是什么样子(Express/NestJS、Django/DRF、Rails + Pundit/CanCanCan、Spring Security、Laravel、Go)。阅读此文件,了解如何在符合惯例的代码中识别检查是否存在,并使用项目自身的惯例编写修复方案。
  • references/patterns.md — “我想安全地实现 X 授权”方案:限定所有者范围的查询、集中式策略层、默认拒绝、从服务器派生身份、安全的多租户范围控制。提出修复方案时应阅读此文件,而不只是指出问题。
  • references/checklist.md — 按端点、资源和应用划分的清单,并附有分流顺序。可用于完整审计、一次扫描多个端点,或对大规模发现的问题进行优先级排序。

这项技能不会做什么

  • 它不会运行应用、发送请求或需要凭据。它只会读取源代码。要在运行时进行确认,请配合动态 BOLA 测试工具(使用两个账户,交换对象 ID)——这是对这项防御性审查的攻方补充。
  • 它不会取代授权引擎(OPA/Rego、AWS Cedar、OpenFGA、Oso)。它会检查现有机制是否实际应用于每条路径,包括昨天新增的路径。
  • 它不会因为可利用性尚未得到证实,就放过缺失的检查。无法确认检查是否存在时,它会标记出来——误报的代价只是留下一条注释;漏报则会导致安全漏洞。
  • 它不会把身份验证当作授权。“你必须登录”永远不能回答“这个用户能否访问这个对象?”。

评论

支持 Markdown