安全
设计安全的 API
大多数严重的 API 安全事故源于授权逻辑,而不是密码学被攻破。本指南涵盖对象级授权、令牌设计、输入校验、限流和服务间信任,以及每项控制应当放在哪里。
· 阅读约 5 分钟 · Starone 工程团队
API 是通往系统数据和操作最直接的路径,而且它本来就是为软件调用而设计的,因此很容易被大批量地探测。真正要紧的失败很少出自什么高深手法,而是这样的端点:认证了调用方,却返回了别人的记录;接受了客户端本不该设置的字段;或者对一项开销很大的操作允许无限次尝试。
我们的观点是,API 安全首先是一门设计上的纪律。网关和 Web 应用防火墙有其用处,但两者都不知道用户 A 能否读取发票 B。这个判断存在于应用之中,而且必须在每一次请求上作出。
对每个对象、每次请求都做授权
对象级授权失效(Broken Object Level Authorization)在 OWASP API Security Top 10 中排在首位,长期以来也被称为 IDOR,是最具代表性的 API 漏洞。端点检查了令牌是否有效,从路径中取出标识符并返回对象,却没有检查这个对象是否属于调用方。只要改一下 URL 里的标识符,另一位客户的数据就暴露了。
UUID 这类不可预测的标识符会让枚举变得更难,但它不是一项控制措施,因为标识符会通过日志、referrer 和其他响应泄露出去。修复必须是结构性的:在查询本身中就按调用方的身份和租户限定每一次数据访问,使范围之外的记录根本查不到。在多租户系统中,数据库的行级安全(row-level security)在应用之下再加一层防护。
功能级授权同样需要认真对待。管理类操作必须在服务端检查角色和权限,不能依赖客户端把按钮藏起来。我们集中定义授权规则,将其作为策略交由中间件或策略引擎求值,对任何没有显式规则的路由一律默认拒绝。
按最小权限设计令牌
认证应当建立在成熟的协议之上,即 OAuth 2.0 和 OpenID Connect,并使用有人持续维护的库来实现。余下的设计决策,都围绕着限制一个被盗的令牌能做什么。
- 让访问令牌保持短有效期,并使用轮换式刷新令牌,一旦发现重用即予吊销。
- 为每个客户端签发范围狭窄的 scope,并在资源服务器上检查;scope 无法替代对象级检查,两者要同时进行。
- 每次请求都校验签名、签发方(issuer)、受众(audience)和过期时间,并固定可接受的算法,不让令牌自行选择算法。
- 浏览器和移动端客户端使用带 PKCE 的授权码流程,并且不让令牌出现在 URL 和日志中。
- 对于高价值 API,考虑通过 DPoP 或双向 TLS 绑定来使用发送方受限的令牌。
在边界处校验输入并控制属性
把请求体直接绑定到持久化模型上会造成批量赋值(mass assignment):客户端加上一个 role、balance 或 ownerId 字段,框架就把它写了进去。把模型直接返回则会造成相反的问题,暴露出界面上从不展示的内部字段。应当为每个端点使用显式的请求和响应 schema,拒绝未知字段,并把任何影响权限、价格或归属的字段都视为由服务端控制。
每个请求都应当在任何业务逻辑运行之前,按严格的 schema 进行校验:类型、格式、长度、范围和允许的取值。在网关或中间件中强制执行的 OpenAPI 定义,使契约成为可执行的,并能尽早拒绝格式错误的输入。校验是对参数化查询和输出编码的补充,而不是替代。
任何会去获取客户端所提供 URL 的端点,例如 webhook 注册或图片导入,都存在服务端请求伪造(SSRF)风险。应当解析目标地址并对照白名单进行检查,屏蔽私有地址段和元数据地址段,并从一个无法访问内部服务的网段发起出站调用。
限制的是资源消耗,而不只是请求速率
限流可以保护可用性,并减缓撞库和枚举攻击。只要存在已认证的身份或 API key,就应当以它作为限流的键,因为按 IP 的限制很容易通过分散到多个地址来绕过,还会不公平地惩罚处于共享网关之后的用户。
- 对认证、密码重置和一次性验证码端点施加更严格的限制。
- 为分页大小、payload 大小、上传大小和执行时间设置上限。
- 对于 GraphQL,要限制查询深度和复杂度,因为一个请求就可能触发数千次 resolver 调用。
- 限制会产生费用的业务动作,例如发送短信或调用付费的第三方 API。
- 返回 429 并附带 Retry-After 头,让行为规范的客户端可以退避。
不要信任内部网络
一种常见的架构是在边缘做认证,然后让内部服务信任任何到达它们的请求。这样一来,只要有一个服务被攻陷,或者出现一个 SSRF 漏洞,攻击者就能在整个系统里畅行无阻。服务之间应当使用双向 TLS 相互认证,采用由服务网格或基于 SPIFFE 的身份系统签发的短有效期工作负载证书,并按服务身份进行授权。
终端用户的身份也必须一路传递。一个只能看到调用方服务身份的下游服务,无法执行对象级规则,这就是混淆代理人(confused deputy)问题。应当用签名的令牌传递用户上下文,最好在每一跳都换成受众更窄的令牌。
清楚自己暴露了什么,并持续监视
没有文档的端点是无法保护的。应当维护一份由 API 定义生成的清单,按公布的时间表下线旧版本,并让调试路由和内部路由远离公网监听器。记录认证和授权决策,包含调用方、对象和结果,同时把令牌和个人数据排除在日志之外。来自同一身份的 403 或 404 响应比例上升,往往是枚举行为最早的迹象。
授权也必须经过测试。用自动化测试以第二个用户、第二个租户的身份调用每一个端点,并断言访问被拒绝,可以发现扫描器发现不了的回归问题。
适度原则与取舍
并非每项控制都适合每个系统。服务间的双向 TLS 会带来证书轮换和调试上的开销;对于一个运行在私有网络上、只有少数几个服务的小系统,网络策略加上签名的服务令牌可能是一个相称的起点。静态 API key 用于低风险的服务器到服务器集成是可以接受的,前提是它有范围限定、可以轮换,并存放在密钥管理服务中。对象级授权、严格的 schema 和消耗限制则不同:无论规模大小,我们都把它们视为没有商量余地的要求,因为它们针对的正是攻击者最先寻找的那些失败。
