
一、引言API 已经成为事实上的数据库大门过去十年应用架构从服务端渲染 Session演进为前后端分离 微服务 移动端 小程序 第三方开放平台。这个演进带来的直接后果是业务逻辑和数据不再藏在页面背后而是以 JSON 的形式裸露在 HTTP 接口上。前端能拿到什么攻击者用 curl 就能拿到什么。更糟的是前端没用到的接口攻击者也能通过逆向 APK、翻 JS 源码、扫 Swagger 文档找到。OWASP API Security Top 10 的两次版本迭代很能说明问题2019 版把失效的对象级授权BOLA“列为 API12023 版依然如此并且把” Excessive Data Exposure过度数据暴露和Mass Assignment批量赋值合并升级为API3: Broken Object Property Level AuthorizationBOPLA对象属性级授权失效。这说明业界踩了五年的坑本质上还是同一类问题——把能访问接口当成了能访问这条数据。本文不谈概念直接从三类高频漏洞出发未授权访问、越权BOLA/BFLA、敏感数据泄露给出可复现的检测代码和可落地的修复方案。在展开具体技术细节之前先看几组真实世界的教训。2021 年Parler 的 API 因为完全没有做对象级授权攻击者只需要遍历自增 ID就能批量下载包括已删除内容在内的所有帖子、图片和视频最终导致整个平台的数据被归档。2022 年Optus 的一个未授权 API 端点泄露了约 1000 万用户的姓名、出生日期、电话号码、邮箱和驾照号码澳大利亚政府直接将其定性为重大网络安全事件。2023 年T-Mobile 的 API 再次被攻破影响 3700 万客户。这些案例的共同点惊人地一致接口语法合法、认证可能有效、但授权逻辑缺失。攻击者不需要复杂的漏洞利用链只需要一个 curl 和一点点耐心。传统 Web 安全时代我们关注 SQL 注入、XSS、CSRF、文件上传这些漏洞的利用往往需要构造特殊的 payload绕过输入过滤。而 API 时代的漏洞利用变得太平凡了——改一个数字、删一个字段、换一个 Token就能拿到本不该拿到的数据。这种低技术门槛、高数据收益的组合让 API 成为黑产和自动化扫描器的首选目标。根据 Salt Security 的 API 安全报告2023 年超过 94% 的生产环境 API 存在安全问题其中 BOLA 占比最高。更值得警惕的是API 攻击的平均检测时间以天甚至周为单位因为流量看起来完全正常——没有异常字符没有攻击特征只有合法用户访问了合法接口只是 ID 不是自己的。二、核心原理认证、授权与对象级盲区2.1 认证Authentication≠ 授权Authorization这是最容易被混淆的一对概念认证回答你是谁——JWT 签名是否有效、Token 是否过期、Session 是否合法授权回答你能干什么——这个用户能不能读这条订单、能不能调这个管理接口、能不能修改这个字段。绝大多数 API 网关Kong、APISIX、Spring Cloud Gateway只做认证和粗粒度路由级鉴权。也就是说只要 Token 有效请求就能打到业务代码。剩下的这个 user_id 是不是当前登录用户网关管不了只能靠业务层。一旦业务层忘了写这一行判断就是一个 BOLA。这里需要进一步拆解认证与授权的技术栈。认证层面常见的方案包括 HTTP Basic用户名密码 Base64、API Key静态密钥、OAuth2授权码、客户端凭证、隐式等模式、JWT自包含令牌、mTLS双向证书。每种方案都有其适用场景但没有任何一种认证方案能解决授权问题。比如 OAuth2 的 access_token 只证明用户授权了客户端访问某些 scope但 scope 通常是粗粒度的如read:orders它不会告诉你这个用户只能读自己的订单。JWT 里的sub声明了用户身份但后端如果不拿sub去和资源属主比对这个身份信息就是摆设。授权层面业界有几种模型RBAC基于角色的访问控制用户→角色→权限。适合功能级授权比如管理员可以删除用户。但 RBAC 无法表达用户只能读自己的订单这种对象级关系。ABAC基于属性的访问控制通过属性用户部门、资源分类、时间、IP动态计算权限。灵活但实现复杂策略容易失控。ReBAC基于关系的访问控制用关系图表达用户 A 是订单 1001 的属主、“用户 B 是项目 X 的成员”。Google 的 Zanzibar 论文是典型代表适合社交、协作类场景。行级安全Row-Level Security在数据库层强制过滤比如 PostgreSQL 的 RLS 策略。这是最后一道防线但很多团队为了性能或便利性把它关掉了。网关之所以做不了对象级授权是因为它看不到业务语义。网关知道这个 Token 有效但它不知道URL 里的 1001 是不是这个 Token 对应的用户。要判断这一点必须查询业务数据库或者至少查询一个权限服务。而网关的设计目标是高吞吐、低延迟、无状态把业务查询塞进网关会破坏它的定位。所以对象级授权天然属于业务层职责但业务层开发者往往只关注功能实现安全校验成了可选项。这就是 BOLA 泛滥的根本原因。2.2 BOLAAPI 安全的头号杀手典型场景GET /api/v1/users/1001/orders Authorization: Bearer 普通用户A的token后端逻辑是app.get(/api/v1/users/{user_id}/orders)deflist_orders(user_id:int):returndb.query(Order).filter(Order.user_iduser_id).all()问题不在于user_id来自路径参数而在于代码从未校验user_id是否等于当前登录用户的 ID。攻击者把 1001 改成 1002、1003就能遍历全站订单。BOLA 之所以排第一是因为它具备三个特征利用成本极低改一个数字、危害面极大数据全量泄露、自动化检测极难需要理解业务语义。传统的 SQL 注入扫描器对它完全无感——请求语法合法、参数合法、响应码 200。BOLA 和 BFLABroken Function Level Authorization功能级授权失效经常被混为一谈但两者有明确区别。BOLA 是你能访问这个对象吗比如普通用户 A 读取用户 B 的订单BFLA 是你能调用这个功能吗比如普通用户调用了DELETE /api/v1/admin/users/123。BFLA 通常更容易发现因为管理接口的路径往往带有/admin、/internal等特征扫描器可以基于路径字典探测。而 BOLA 的路径完全正常参数完全正常只有资源属主不对扫描器需要知道这个 ID 属于谁才能判断这就是为什么 BOLA 检测如此困难。BOLA 还有几个变种很多开发者只防了路径参数却漏了其他位置路径参数越权GET /users/{id}/orders改id。查询参数越权GET /orders?user_id1001改user_id。开发者可能只校验了路径参数忘了查询参数。请求体越权POST /ordersbody 里带{user_id: 1001}后端直接用 body 里的user_id创建订单而不是用 Token 里的sub。攻击者可以给他人创建订单或者把自己的订单挂到别人名下。批量接口越权POST /orders/batchbody 里是一个 ID 数组后端只校验了第一个 ID后面的直接查询。攻击者可以在数组里混入他人的 ID。嵌套资源越权GET /teams/{team_id}/projects/{project_id}只校验了team_id的成员关系没校验project_id是否属于该 team。攻击者可以跨团队读取项目。检测 BOLA 的自动化思路是准备两个测试账号A 和 B用 A 的 Token 去访问 B 的资源。如果返回 200 且包含 B 的真实数据就是 BOLA。但这里有一个工程难点如何知道 B 的资源 ID对于自增 ID可以直接枚举对于 UUID需要从 B 的正常响应中提取或者通过其他接口泄露。这也是为什么很多团队在 API 设计时使用 UUID 而不是自增 ID——虽然这不能根治 BOLA但能提高自动化枚举的成本。不过UUID 不是安全措施只是增加了攻击者获取 ID 的难度不能替代授权校验。2.3 敏感数据泄露的三条路径敏感数据泄露通常不是被拖库而是被合法接口吐出来响应体过度暴露后端直接把 ORM 实体序列化返回password_hash、id_card、internal_remark全部带出去属性级越权Mass Assignment注册接口接收整个 JSON 并user.update(**body)攻击者塞入{role: admin}接口资产暴露/actuator/env、/v3/api-docs、/graphql的 introspection 未做访问控制直接把内部接口清单交给攻击者。除了这三条还有几条容易被忽视的路径错误信息泄露后端抛出异常时返回完整堆栈暴露数据库表名、SQL 语句、文件路径、依赖版本。攻击者可以据此推断数据库结构甚至找到已知漏洞的组件版本。日志与监控接口泄露/actuator/loggers、/actuator/heapdump、/debug/pprof等端点如果暴露攻击者可以下载堆内存、修改日志级别、甚至执行任意代码。Spring Boot Actuator 的/env和/configprops曾经是重灾区。GraphQL 过度查询GraphQL 的 introspection 默认开启攻击者可以获取完整的 schema然后构造深层嵌套查询一次性拉取大量关联数据。如果后端没有做查询深度限制和复杂度限制一个请求就能拖走整个数据库。第三方 SDK 与埋点泄露移动端或前端集成的第三方 SDK 可能把用户数据发送到外部服务器或者在前端代码中硬编码了 API Key、数据库连接串。批量接口与分页泄露GET /api/v1/users?page_size100000后端没有限制分页大小攻击者可以一次性拉取全量用户。或者GET /api/v1/orders/export导出接口没有做权限校验直接生成包含所有订单的 CSV。2022 年 Optus 的泄露事件就是一个典型攻击者发现了一个未授权的 API 端点该端点通过自增 ID 暴露了用户数据而且没有做速率限制。攻击者只需要写一个简单的循环就能批量获取 1000 万用户的信息。这个案例同时命中了 BOLA、过度数据暴露和接口资产暴露三个问题。三、实战案例三个场景的复现与修复以下代码仅用于授权范围内的安全测试。未授权对第三方系统进行扫描属于违法行为。3.1 案例一JWT 校验形同虚设很多团队用 JWT 时只做了jwt.decode(token)而没有强制指定算法。这会导致两类经典攻击alg: none绕过以及 RS256 公钥当 HS256 密钥使用。检测脚本如下importbase64,json,requestsdefb64url_decode(s:str)-bytes:returnbase64.urlsafe_b64decode(s*(-len(s)%4))defb64url_encode(b:bytes)-str:returnbase64.urlsafe_b64encode(b).rstrip(b).decode()defparse(token:str):h,p,_token.split(.)returnjson.loads(b64url_decode(h)),json.loads(b64url_decode(p))defprobe_alg_none(token:str,url:str)-bool:构造 algnone 的伪造 token验证服务端是否放行header,payloadparse(token)header[alg]noneforgedf{b64url_encode(json.dumps(header).encode())}.\f{b64url_encode(json.dumps(payload).encode())}.rrequests.get(url,headers{Authorization:fBearer{forged}},timeout5)returnr.status_code200ifprobe_alg_none(TOKEN,https://api.example.com/v1/me):print([!] 高危服务端接受 algnone认证可被完全绕过)修复的核心只有一句话解码时白名单算法并校验签名。importjwtfromjwtimportPyJWKClient jwks_clientPyJWKClient(https://auth.example.com/.well-known/jwks.json)defverify(token:str)-dict:signing_keyjwks_client.get_signing_key_from_jwt(token)returnjwt.decode(token,signing_key.key,algorithms[RS256],# 显式白名单杜绝 none / HS256 混淆audienceapi.example.com,issuerhttps://auth.example.com,options{require:[exp,iat,sub,aud]},)options.require这一项经常被忽略。缺少exp的 Token 就是永久凭证一旦泄露无法吊销。要理解这些攻击需要先看清 JWT 的结构。JWT 由三部分组成Header、Payload、Signature用点号连接。Header 是一个 JSON包含alg签名算法和typ类型Payload 包含声明claims如sub主体、exp过期时间、iat签发时间、aud受众、iss签发者Signature 是对前两部分的签名用于防篡改。alg: none攻击的原理是JWT 规范允许alg为none表示不签名。如果服务端在解码时没有指定算法白名单而是信任 Token 头部的alg攻击者就可以把alg改成none去掉签名伪造任意 Payload。很多 JWT 库的默认行为是根据 Token 头部的 alg 选择验证算法这就给了攻击者可乘之机。RS256/HS256 混淆攻击的原理是RS256 使用非对称密钥私钥签名、公钥验签HS256 使用对称密钥同一个密钥签名和验签。如果服务端配置为 RS256但攻击者把alg改成 HS256然后用公钥作为 HMAC 密钥签名而服务端的 JWT 库又恰好支持 HS256就会用公钥去验签结果验证通过。公钥通常是公开的通过 JWKS 端点所以攻击者可以轻松获取。除了这两种JWT 还有几个坑kid注入kid是 Header 里的密钥 ID如果服务端直接用kid去查文件或数据库攻击者可以构造kid: ../../etc/passwd或者 SQL 注入实现路径穿越或注入攻击。jku/x5u注入jku指定 JWKS 的 URL如果服务端信任 Token 里的jku攻击者可以把它指向自己的服务器用自己的密钥签名服务端就会用自己的公钥验签通过。弱密钥爆破HS256 如果使用弱密钥如secret、123456攻击者可以用 hashcat 快速爆破。Token 泄露JWT 通常存在 localStorage容易受到 XSS 攻击。如果存在 Cookie又可能受到 CSRF 攻击。推荐使用HttpOnlySecureSameSite的 Cookie并配合 CSRF Token。FAQ为什么不能只用jwt.decode因为jwt.decode默认不验证签名它只是 Base64 解码。要验证签名必须用jwt.decode(token, key, algorithms[...])并显式指定算法列表。PyJWT 从 2.0 开始强制要求指定algorithms这是一个重要的安全改进。FAQ如何测试 JWT 安全性可以按照以下清单检查1是否接受alg: none2是否混淆 RS256/HS2563是否校验exp、iat、nbf4是否校验aud、iss5kid是否可注入6jku是否可篡改7密钥是否足够强8Token 存储位置是否安全9是否有吊销机制。对于第 9 点JWT 是无状态的默认无法吊销需要配合黑名单或短过期时间 Refresh Token。3.更多硬核网安与AI工具包请扫码获取完整源码2 案例二BOLA