原文:jviide - 2024.05.23
TL;DR: 与其将 API 调用从 HTTP 重定向到 HTTPS,不如让失败显而易见。要么完全禁用 HTTP 接口,要么返回明确的 HTTP 错误响应,并撤销通过未加密连接发送的 API 密钥。遗憾的是,许多知名的 API 提供商目前并没有这样做。
背景
当用户将网络浏览器指向 HTTP URL 时,服务通常会将请求重定向到相应的 HTTPS 页面。通信流的这一未加密部分有其缺陷。共享网络中的第三方和网络中介可以从最初的 HTTP 流量中嗅探密码和其他机密,甚至通过 MITM 中间人攻击冒充网络服务器。尽管如此,重定向技术在从基本不加密的早期网络向大部分加密的当今网络过渡的过程中,迈出了有用的第一步。
后来的技术进一步加强了安全性。现在,服务器可以在发送 HTTP 到 HTTPS 的初始重定向响应的同时发送 HSTS(HTTP Strict-Transport-Security
),告诉用户的浏览器从此只能在该域使用 HTTPS。这就将琐碎的嗅探和 MITM 中间人攻击的机会窗口限制在了第一次请求。随后,浏览器增加了 HSTS 预加载列表和 HTTPS-Only 模式,允许完全跳过初始未加密请求。
从可用性和安全性权衡的角度来看,这一切对于面向用户的网站都是合理的。但有趣的是,重定向方法似乎也被 API 广泛采用。API 主要由其他软件使用,因此同样的可用性论点在这里并不适用。此外,许多 API 客户端往往不会像浏览器一样保留他们所看到的 HSTS 标头状态。
本文认为,由于这些因素,应重新考虑将 API 调用从 HTTP 重定向到 HTTPS 的常见做法。虽然本文主要针对 REST API,但其观点也适用于使用 HTTP(S) 作为传输机制的其他类型的 API。
一个简单的拼写错误就足够了
在工作中,我们正在针对第三方 API 构建一个新的集成。初始代码提交中包含了一个拼写错误的 API 基础 URL,写成了 "http://..."
而不是 "https://..."
。这是一个相当容易犯的错误。
这个错误在运行时基本上被掩盖了:第三方 API 对每个请求都会响应 301 重定向到其 HTTPS 端。Node.js 内置的 fetch 功能会愉快且悄悄地 跟随访问这些重定向到 HTTPS 端点。
(注:Go 语言默认的 http.Client
也会自动请求重定向后的端点,除非自定义 CheckRedirect
属性。)
现在,这样的 API 请求都将 API 密钥以明文形式发送到网络上,然后再将其发送到加密端点。一个字母的遗漏可能会在我们不知情的情况下将使用的 API 密钥暴露给第三方。随着集成的进行,代码很有可能会在数年之间,泄露 API 调用中的任何机密。从长远来看,恶意的可能性会不断累积。
幸运的是,我们在代码审查期间发现了这个错误,避免了错误扩散到生产甚至测试中。我们还意识到,我们自己的 API 也进行了类似的 HTTP 到 HTTPS 重定向。
快速失败原则
当 API 将 HTTP 请求重定向到 HTTPS,而 API 客户端默默地跟随访问这些重定向时,它往往会隐藏像上述案例中的拼写错误。一个简单的字母遗漏很容易被忽略,最终进入生产环境,危及整个系统的机密性。
在大多数情况下,最好遵循快速失败原则:未加密的 API 调用应以明显可见的方式失败,这样开发人员可以在开发过程中尽早发现并修复拼写错误。
快速失败最好的解决方案是完全禁用 API 服务器的 HTTP 接口,不再响应对 80 端口的连接尝试。如果初始的未加密连接从未建立,那么就不会发送 API 密钥,从而减轻嗅探攻击的风险,并将 MITM 中间人攻击的机会窗口限制在极小的时间范围内。这种方法适用于以自己的域名(如 api.example.com
)托管的 API。
我们自己的 API 位于 /api
路径下,与我们服务的 Web UI 在同一个域名下。因此,我们对完全禁用 HTTP 接口没有信心,所以选择了第二种方案:所有在 /api
路径下的未加密 HTTP 请求,现在都返回描述性错误信息和 HTTP 状态码 403。在开发过程中可能会出现一些初始明文请求,但开发人员更容易注意到它们。
还有谁?
这解决了我们自己的 API 问题。我们还联系了第三方 API 提供商和几个朋友,让他们检查一下自己的 API。谁知道呢,也许有一些常用的 API 接受 API 密钥(或其他凭证),并且也从 HTTP 重定向到 HTTPS?
我从脑海中列出了一些知名的 API,并做了一个小调查。其中有几个返回 HTTP 错误或完全拒绝连接。这里列出了它们的 cURL 命令,以便查看它们的详细响应:
- Stripe API:返回 403 (“Forbidden”) 和一个描述性错误信息。
curl -i http://api.stripe.com
- NPM Registry API:返回 426 (“Upgrade Required”) 和一个描述性错误信息。
curl -i -X PUT -H 'content-type: application/json' -d '{}' 'http://registry.npmjs.org/-/user/org.couchdb.user:npm'
- Fastmail JMAP API:整个 HTTP 接口似乎已被禁用。
curl -i -H 'Authorization: Bearer foo' http://api.fastmail.com/jmap/session
- …
然而,以下 API 确实 响应了 HTTP 到 HTTPS 的重定向:
- Facebook Graph API
curl -i 'http://graph.facebook.com/me?access_token=foo'
- IBM Cloud API
curl -i "http://iam.cloud.ibm.com/identity/token" -d "apikey=YOUR_API_KEY_HERE&grant_type=urn%3Aibm%3Aparams%3Aoauth%3Agrant-type%3Aapikey" -H "Content-Type: application/x-www-form-urlencoded" -H "Authorization: Basic Yng6Yng="
- OpenAI API — 更新于 2024-05-28,已返回错误。
curl -i -H "Content-Type: application/json" -H "Authorization: Bearer 123" -d '{}' http://api.openai.com/v1/chat/completions
- …
我没有单独向所有这些 API 提供商报告这些发现。有一些未列出的异常,我确实联系了它们,但结果各不相同。
请谨慎对待每个结果:我不得不在没有有效凭证的情况下测试其中一些 API,或者使用文档示例中的凭证。但总体模式表明,API 将 HTTP 请求重定向到 HTTPS 的习惯非常普遍。为什么会这样呢?
最佳实践也需要实践
当与他人谈论这个话题时,许多人都指出,API 从 HTTP 重定向到 HTTPS 的缺点是显而易见的——事后看来是这样。
面向用户的应用程序的重定向经常在最佳实践列表和备忘录中被提及,例如由 OWASP(开放网络应用安全项目)发布的那些。相反,专门针对 API 的建议似乎很少。我只在 OWASP 网站的深处发现了几处提及,例如一个出色的 PDF 幻灯片集,由 Philippe De Ryck 编写,名为"常见 API 安全陷阱":
“常见 API 安全陷阱”的第 8 张幻灯片,重点强调了相关部分。
我的 Google 搜索技巧可能不够好。不过,也许每个建议面向用户的网站进行 HTTP 到 HTTPS 重定向的最佳实践项目,都应该附加明确的注意事项,在显著位置建议不要对 API 进行此类重定向。因此,我在 OWASP 的 GitHub 页面上发起了一个议题,建议相应修改 OWASP 的传输层安全备忘单。
额外内容:以明文响应的流行 API
在审查 API 列表时,我碰到了一些既不重定向也不拒绝未加密请求的流行 API。它们只是以未加密的 HTTP 响应回应未加密的 HTTP 请求,没有在任何阶段强制使用 HTTPS。
也许它们有自己的原因,或者只是意外地错误配置了它们的反向代理。不管怎样,考虑到它们都处理潜在的敏感数据,我通过各自的安全渠道联系了这些 API 提供商,并解释了这个问题。
结论
由于 API 的性质,将 HTTP 重定向到 HTTPS 可能弊大于利。与面向用户的网页不同,API 主要由其他软件使用。API 客户端通常会自动跟随重定向,并且不会维护状态或支持诸如 HSTS 之类的安全标头。这可能导致静默故障,即每个 API 请求中的敏感数据最初都是以明文形式在网络上传输的,没有经过加密。
让我们采用“快速失败”的方法,完全禁用 HTTP 接口或对未加密请求返回明确的错误响应。这可以确保开发人员能够快速注意并修复意外的 http://
URL 为 https://
。我们应将通过未加密连接发送的 API 凭证视为泄露,并立即自动撤销。
在撰写本文时,一些知名且流行的 API 确实将 HTTP 请求重定向到 HTTPS。这种行为似乎很普遍,也许是时候修改最佳实践,明确建议 API 拒绝处理未加密的请求了。