在接口设计阶段落地 OWASP API 安全 Top 10:以对象级授权(BOLA)防护为例

枫少@KillBoy
枫少@KillBoy
枫少@KillBoy
管理员
267
文章
0
粉丝
安全开发28787字数 2760阅读9分12秒阅读模式
AI智能摘要
AI 生成的文章内容摘要

评审一份接口设计文档时,最容易通过的往往是那些看起来最普通的接口:GET /orders/{id}PATCH /profiles/{id}DELETE /files/{id}。文档里写了"需要登录后访问",评审就过了。但"登录后访问"和"只能访问自己的对象"是两回事,把 URL 里的 1001 换成 1002 就能读到别人订单的问题,正是 OWASP API 安全 Top 10 里排在 API1:2023 的失效对象级授权(BOLA)——接口没有校验已认证用户是否有权访问这个具体对象。这类缺陷不需要绕过认证,不留下异常特征,靠上线后的扫描去补往往已经晚了。下面这份清单的用途很直接:下次评审接口设计文档时,逐条对着接口列表勾。

接口设计评审中对象级授权检查的示意插画

为什么越权总是从设计阶段漏出去

设计文档的写法决定了它会漏掉什么。多数接口文档的结构是路径、方法、请求参数、响应字段、错误码,认证一栏通常只有"需要 Token"这一句。这套模板天然记录"谁能调这个接口",却没有位置记录"这个用户能操作哪些对象"。于是授权判断被默认推给了实现阶段,而实现阶段的开发者看到文档里 id 是必填参数,最自然的写法就是拿着 id 去查库返回。

第二个原因是职责错位。认证由网关、中间件或统一框架层完成,看起来"已经有一层安全",但网关只知道请求带着一个有效身份,不知道路径里那个 ID 属于谁——对象归属关系存在业务数据里,只有业务代码能判断。任何把授权指望在统一入口一次解决的设计,在按 ID 操作的接口上都会留下缺口。

第三个原因是评审视角。评审时大家更容易讨论字段命名、分页方式、幂等性和错误码,因为这些有明确的对错;而"越权"要靠想象一个恶意调用者才能发现。所以有效的做法不是提醒大家"注意安全",而是把三个必答问题写进评审清单,让漏答变成一个可见的缺项。

第一步:每个按对象 ID 操作的接口,是否校验归属权

先把接口清单过一遍,标出所有请求参数中包含对象标识的接口——路径参数、查询参数、请求体字段、批量数组里的 ID,都算。这一步的输出应该是一张明确的"受保护对象接口表",而不是印象中的"大概几个"。批量接口和嵌套资源最容易被忽略:一个接受 ID 数组的批量删除接口,如果只校验了数组里第一个元素,等于没校验。

对每个标出的接口,文档里必须能回答三件事:这个对象归属于谁(用户、租户、组织、还是某个上级资源);归属关系从哪里查出来(对象自身字段、关联表、还是外部服务);校验失败时返回什么。归属关系的表述必须落到具体字段或关联路径,"由业务层判断权限"这种描述在评审时应当直接退回。

实现层面的关键是把归属条件写进数据访问本身,而不是先查出对象再比对。查询条件同时包含对象标识和当前身份,越权请求自然查不到数据:

# 有缺陷的写法:先取对象,再看要不要拦
order = repo.find_by_id(order_id)
return order          # 没有任何归属判断

# 修正示意:归属条件进入查询条件
order = repo.find_one(id=order_id, owner_id=current_user.id)
if order is None:
    raise NotFound()  # 不存在与无权访问返回一致

返回码的选择也值得在文档里定下来。对无权访问的对象返回"不存在",可以避免通过响应差异确认对象是否存在;如果业务上必须区分,就要明确这是有意接受的信息暴露。另一个常被提到的辅助手段是用不可预测的标识替代连续自增 ID,但它只增加猜测成本,不能替代归属校验,评审时不应把"我们用了随机 ID"当作这一项的答案。

第二步:列表与详情接口,是否按最小权限裁剪字段

列表接口的风险和详情接口不同:它通常没有对象 ID 参数,越权发生在查询范围上。评审时要确认列表查询的过滤条件是服务端根据当前身份强制附加的,还是来自请求参数。凡是由客户端传入 user_idtenant_idorg_id 之类的范围参数并直接进入查询的设计,都要求给出服务端二次校验的说明;能从会话身份推导的范围参数,最好在设计阶段就从接口签名里删掉。

排序、筛选、搜索关键字和导出功能是同一问题的变体。允许按任意字段筛选,等于把数据库查询能力开放给调用者;允许导出全量,等于把分页限制绕过。这些能力如果保留,就需要写明可用字段白名单和范围约束。

字段维度还有一层更细的问题——对象内部的属性授权。同一个对象,不同角色能看到的字段不该相同:客服看得到订单联系人,普通用户不该看到风控标记或内部备注。稳妥的做法是响应结构按调用者角色显式定义,用输出模型组装返回内容,而不是把数据库实体直接序列化。写入方向同理,更新接口应当明确可写字段白名单,避免请求体里多带一个 roleowner_id 就改掉了归属关系。评审时可以问一句:这个响应结构是列出来的,还是模型直接吐出来的?

第三步:更新与删除接口,是否重复鉴权

读接口的越权是数据泄露,写接口的越权是数据被改写或删除,后者往往不可回滚。但写接口恰恰更容易漏检,因为流程里常常已经有过一次校验:详情页打开时校验过,提交时就默认"这个对象已经确认过归属"。这种依赖前序请求的假设在 API 场景下不成立——调用者可以直接构造提交请求,跳过任何前置步骤。

所以规则很简单:每一次状态变更都要独立完成归属校验,把校验条件写进更新和删除语句的条件里,而不是先查再改。多步流程(草稿、预校验、确认提交)的每一步都要各自校验,不要把中间态令牌当作授权凭据。状态机类接口还要加一层判断:当前身份是否有权执行这个状态转换,而不只是是否拥有这个对象——拥有一个订单并不意味着可以把它改成"已退款"。

写接口的评审还应确认审计记录是否包含操作者身份、目标对象标识和结果。归属校验失败的请求同样值得记录,集中出现的失败往往是有人在遍历 ID。

典型漏检场景与整改方向

把下面这些场景直接当作评审时的对照项,出现一条就回到对应的检查步骤:

漏检场景为什么会被放过整改方向
嵌套资源只校验了父对象认为路径前缀已经限定范围校验子对象确实属于该父对象,且父对象属于当前身份
批量接口按数组接收 ID单个接口有校验,批量走了另一条代码路径逐元素校验,任一失败即整批拒绝
客户端传入范围参数前端本来就只会传自己的 ID范围从会话身份推导,或服务端二次比对
内部接口、管理后台接口认为不对外暴露按同等标准校验,不依赖网络位置作为授权
文件下载与导出链接只校验了链接本身有效校验请求者对文件所属对象的权限,并限制有效期
第三方回调与集成接口有签名校验就算通过签名只证明来源,仍需校验对象归属与可操作范围
多租户共用查询层租户字段在上层拼装租户隔离下沉到数据访问层,禁止绕过的查询入口

整改的共同点是把授权条件从"流程中的某一步"变成"数据访问的固有条件"。只要还存在一条能拿到对象却不带身份约束的查询路径,就迟早会有接口调用它。

让清单持续生效

一次性评审只能清理存量。要让它在后续迭代中继续起作用,可以把检查绑到变更上:凡是新增或修改了接口、认证方式、数据模型的改动,都要求同时更新受保护对象接口表,并在代码评审中回答归属校验写在哪里。有公开的实践建议提出,在每个涉及接口、认证或数据模型变更的合并请求上运行接口规格审计与动态测试,对高风险接口在预发环境定期做一轮 BOLA、认证与注入的回归测试,生产环境保持运行时防护常开。这套节奏是否完全照搬要看团队规模,但方向值得参考:授权测试的触发条件应该是"接口变了",不是"发版前想起来了"。

测试用例的写法也建议固定下来:为每个受保护对象接口准备两个身份,用 A 的凭据访问 B 的对象,断言拿不到数据也改不动状态。这类用例写起来不复杂,价值在于它会随接口一起长期存在,新接口漏了校验时会立刻失败。

下一步可以从最小动作开始:打开当前的接口设计文档,找出所有带对象标识的接口,在每一条后面补上"归属字段"和"校验位置"两栏。填不出来的那几行,就是这次评审真正要讨论的内容。

 
枫少@KillBoy
评论  28  访客  28
    • 夕阳箫鼓
      夕阳箫鼓 0

      把归属条件写进查询条件这条太实用了

        • 社牛小飞蛾
          社牛小飞蛾 0

          @ 夕阳箫鼓 实际写的时候要注意,归属条件必须进WHERE,不能放在业务层做二次判断

        • 梦幻小熊
          梦幻小熊 1

          批量接口只校验第一个元素这点确实容易踩坑

          • 薰衣草紫
            薰衣草紫 1

            批量接口只校验第一个元素,我们真踩过

            • ClawCuddler
              ClawCuddler 1

              管理后台一直靠内网当授权,看完有点慌

              • 孤独的月
                孤独的月 1

                两个身份互相访问的测试用例,明天就补上

                • 黑曜圣使
                  黑曜圣使 1

                  无权访问返回404还是403,团队里吵过好几轮

                    • 星雾梦游者
                      星雾梦游者 1

                      @ 黑曜圣使 我们最后选了404,主要是怕暴露对象存在性

                    • 吐蕃使者
                      吐蕃使者 1

                      最难的是让评审时真有人问出这句话

                      • 代码渔夫
                        代码渔夫 1

                        接口表那两栏,填不出来的行确实最要命

                          • 向日葵
                            向日葵 1

                            @ 代码渔夫 对,这两栏就是评审时的照妖镜,能填出来的才是真想过授权

                          • 数据森林守护者
                            数据森林守护者 1

                            第三方回调那条提醒到了,我们只验了签名

                            • Vibe Curator
                              Vibe Curator 1

                              导出功能绕过分页,说得很准

                              • 迷梦之舟
                                迷梦之舟 1

                                多租户下沉到数据访问层,改造成本大概多少?

                                • 避世小鹿
                                  避世小鹿 1

                                  嵌套资源那条我们真吃过亏,父对象校验过了子对象没管

                                  • 辣椒炒肉丝
                                    辣椒炒肉丝 1

                                    导出全量数据这个点很多人会漏,评审时可以专门问一句

                                    • 沙漠独行者
                                      沙漠独行者 1

                                      测试用例用两个身份互访,这招挺狠

                                        • 云旅
                                          云旅 1

                                          @ 沙漠独行者 双身份互访这套加进回归里最省事,一改权限就能跑一遍,比人工点着看靠谱多了

                                        • 龙吟阁
                                          龙吟阁 1

                                          管理后台依赖内网这点太真实了,换个思路就全暴露

                                          • 刺猬战士
                                            刺猬战士 1

                                            状态机的权限转换容易被忽略,拥有对象不代表能执行所有状态变更

                                            • 不拘一格
                                              不拘一格 0

                                              测试用例固定下来是个好做法,新接口漏了直接报红

                                              • 灰烬之语
                                                灰烬之语 1

                                                “由业务层判断权限”这句太眼熟了,我们文档里满屏都是

                                                • 蘅芜苑人
                                                  蘅芜苑人 1

                                                  字段级授权那层很多人只做到接口级,角色不同返回内容应该不一样

                                                  • 像素深渊
                                                    像素深渊 1

                                                    用随机ID当越权防护这个坑我们踩过,最后还是老老实实加归属校验。

                                                      • 幻影剑影
                                                        幻影剑影 1

                                                        @ 像素深渊 随机ID顶多让人猜不到,ID一泄漏照样被翻,归属校验还是绕不过去😂

                                                      • 欢脱袋鼠
                                                        欢脱袋鼠 1

                                                        批量接口的逐元素校验记得写进文档,别只靠代码实现

                                                        • 虚拟领主
                                                          虚拟领主 1

                                                          详情页校验过就默认提交也安全,这个坑太常见了

                                                          • 幽潭
                                                            幽潭 1

                                                            评审清单里加上这三个问题,比喊注意安全管用多了

                                                          匿名

                                                          发表评论

                                                          匿名网友

                                                          拖动滑块以完成验证