预签名 URL 在技术上是一种安全漏洞Presigned URLs are technically a security vuln
预签名 URL 本质上是一种故意的重放攻击,而可重放的身份验证令牌是制造脆弱系统的教科书式方法。尽管 Tigris 和几乎所有其他对象存储系统都将其作为一等功能提供,但这并非疏忽。预签名 URL 将重放攻击这一弱点转化为一种特性,解决了传统身份验证修复方案的痛苦。作者认为,在特定场景下,这种权衡是合理且必要的。
Xe Iaso
预签名 URL 就是你故意为之的一次重放攻击。
可重放的认证令牌是制造脆弱系统的教科书式方法,但 Tigris 将其作为预签名 URL 的一等公民功能推出,地球上所有其他对象存储系统也是如此。然而,这并非疏忽,因为预签名 URL 将弱点变成了功能。
重放攻击是一个真实存在的问题,而经典的修复方法令人痛苦。
当你使用 Amazon 的 SigV4 协议为 Tigris 认证请求时,你的客户端会将请求简化为规范形式:对请求的方法、路径、查询参数、签名头进行 SHA256 哈希,以及对负载进行 SHA256 哈希。然后使用从你的秘密访问密钥派生出的签名密钥,通过 HMAC 处理该结果。没有任何机密信息会通过网络传输。服务器派生出与客户端相同的密钥,执行相同的规范形式转换,并比较结果。
能够生成有效签名证明了请求来自持有秘密访问密钥的人,但它无法证明请求是何时发出的。一年前生成的签名在今天或你发送它的任何其他时间仍然有效,因此理论上攻击者可以囤积你已签名的请求,然后在稍后大规模重放。想象一下,手里攥着一大堆已签名的“创建 EC2 实例”调用,只为了在日后将它们全部狂发出去。你就会成为一个捻着胡须的反派,能够随时生成数十台服务器。
传统上,解决方法是将一个 nonce(一次性使用的数字)烘焙到签名中(向在场的英国读者致歉)。这使得每个签名都不同,因为 nonce 不同。
然而,能力越大责任越大,确保一次性使用的东西只被使用一次是一个出奇困难的分布式系统问题。你无法在本地验证某物只被使用了一次。假设你将它们全部存储在一个 15 分钟的滑动窗口内,请求速率较低,比如 10,000 Bq。那就是 900 万个活跃的 nonce,而且每个前端节点都需要在不断变化时对整个集合保持一致的视图。
你为了确保事物只被使用一次,把原本快速的认证检查变慢了。
相反,你想要的是一种无需协调就能不断变化,并免费使那些旧签名失效的东西。作为额外奖励,你还希望它也存在于每种编程语言的标准库中。
对时钟进行签名
恰好有一个值在不断地、(主要)单调地变化,并且已经在整个技术栈的所有元素中进行了主动协调:时钟。你的操作系统已经与公共 NTP 池保持时间同步(如果你酷到到处都有放射性 PCI 卡,那就是私有 NTP 池)。如果没有准确的时间视图,你就无法建立 TLS 连接,这意味着你根本无法向 Tigris 发出 API 调用,因此认证层可以假定存在一个可用的时钟。
SigV4 将当前时间签名到请求中。如果攻击者用他们那肮脏的黑客之手获取了签名,他们大约有 15 分钟的时间来使用它,之后它就会变成一个数字镇纸。如果时间是签名的输入,并且时间变化足以使签名失效,那么该签名就无效了。当然,在理论上,资金充足的攻击者可以在你的数据中心制造一个黑洞并扰乱时间流,但在那种情况下,地球可能已经完蛋了,这使得这种攻击场景毫无意义。用这个巧妙的技巧实施大规模对象存储欺诈吧!时间调查部门肯定会讨厌它的!
这使得你的验证保持无状态。所有内容都会根据服务器已经需要的系统时钟进行检查,并且你可以给客户端一个 15 分钟的签名滑动窗口,作为针对老旧或延迟客户端的宽限期(指数退避是件好事,Tigris 会因为你这样做而奖励你)。
当然,在传输过程中真正保护签名安全的是 TLS (HTTPS)。如果它被破坏了,我们会有更大的麻烦,而对象存储欺诈是我们面临的最小问题。
时间是你唯一需要的 nonce,因为双方反正都已经对此达成了一致。
有些荆棘上也会开出玫瑰
预签名 URL 拿走了 SigV4 花费所有精力去削弱的重放容忍度,然后将其增强为一项功能。整个认证流程被扁平化为任何 HTTP 客户端都可以使用的 URL 参数,无论是浏览器、curl、Go 的 net/http,还是你通过 socket 比特拨号手写的 HTTP 客户端。这是一个我拆解出来以供展示的真实预签名 URL:
https://xe-sophia-base.t3.tigrisfiles.io/moby-dick.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=tid_ubYBNEYAmTciLVwszw_QrUXDmtcyQisryryGfxgznDsCnOvNqh/20260714/auto/s3/aws4_request&X-Amz-Date=20260714T043308Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=0dcaf4972911527a7582ff36ea457e9760a8efccb6655a178685aaa281637a36以下是各个部分(原谅这看起来像 AI 生成的列表,因为这确实是格式化此内容的最佳方式):
在标准的 SigV4 请求中,所有这些通常都是 HTTP 头。
GET /moby-dick.txt HTTP/1.1Host: xe-sophia-base.t3.tigrisfiles.ioX-Amz-Date: 20260714T043308ZX-Amz-Content-Sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855Authorization: AWS4-HMAC-SHA256Credential=tid_ubYBNEYAmTciLVwszw_QrUXDmtcyQisryryGfxgznDsCnOvNqh/20260714/auto/s3/aws4_requestSignedHeaders=host;x-amz-content-sha256;x-amz-dateSignature=0dcaf4972911527a7582ff36ea457e9760a8efccb6655a178685aaa281637a36请注意,此请求并非合法请求,它只是一个用来说明问题的示例,此处有龙,等等等等。
最好将这个预签名 URL 视为一种能力授权。持有它的人可以对一个 (1) 存储桶中的一个 (1) 对象使用一种 (1) HTTP 方法进行恰好一种 (1) 类型的 API 调用。在预签名 URL 过期之前,他们可以随心所欲地执行多少次都可以。签名涵盖了方法、路径和已签名的头,因此用户无法获取一个用于从开发环境 GET 一份《白鲸记》副本的预签名请求,并将其武器化为删除生产存储桶中所有内容的方法。
占有即授权,直到时钟说不。
你需要付出的代价
像这样的能力授权可能会存在一些棘手的问题。除了销毁用于签名的访问密钥外,没有真正的方法可以撤销单个预签名 URL。当该密钥失效时,它签名的所有内容也会随之失效。这包括你可能想要保留的任何 URL。这种做法有利有弊,而且除非你为每个预签名请求创建一个新的密钥对(这可能超出了讨论范围),否则只能如此。
过期时间也有其附加细则。预签名请求的有效期可以短至一 (1) 秒,长至一 (1) 周(七 (7) 个二十四 (24) 小时的周期)。
客户端使用预签名请求的次数没有限制。如果你给一只老鼠权限去 GET 一块饼干,它们就可以一遍又一遍地 GET 同一块饼干。最终你不得不为这些 GetObject 调用买单,所以请牢记这一点。
URL 也会泄露,但这些 URL 生来就是要消亡的。预签名 URL 最终会出现在 API 响应、聊天消息、GitHub 评论以及你的浏览器历史记录中。这种权衡是可以接受的,因为所有链接都会自动销毁,但在设计服务时,你需要牢记这是一种权衡,而不是访问控制的万能药。
预签名 URL 听起来是防止盗链的好方法。在某种程度上确实如此(我的一些服务就是这样使用它们的),但它们实际做的是给盗链加上了有效期。这会让事情变得足够令人厌烦,通常能让人们停止盗链。
围栏上的洞就是那扇门
SigV4 让许多 API 身份验证挑战变得容易得多。它将大部分创新精力花在让签名快速失效上,因为重放攻击是签名请求出错的典型方式。预签名 URL 看到了这个特性,耸了耸肩,将其彻底反转,并把它变成了一项功能。
那个看起来像问题的东西,变成了构建应用程序的基础结构。
想要分发会自动过期的链接吗?
Tigris 在全球分布且兼容 S3 的对象存储上开箱即用地支持预签名 URL,使用的是你已经熟悉的那套 SigV4 流程。
需要完整排版与评论请前往来源站点阅读。