分析 API 密钥在依赖包中的泄露风险,提出防范策略。对所有调用 LLM 的开发者都有重要参考。
如果今天有人破坏了你项目的某个依赖项,他们能够窃取你的 OpenAI、Anthropic 或 Gemini API 密钥吗?
答案并不取决于你使用哪个 LLM 提供商或你的代码库有多安全。它主要取决于一个许多团队从未考虑过的架构决策:当应用程序运行时,你的提供商 API 密钥实际上存放在哪里。
如果该密钥存放在应用程序自身的进程中,在该进程中运行的每个依赖项都共享相同的环境。受损的包不需要侵入你的基础设施。它只需以与应用程序相同的权限执行,就能访问你的代码可以访问的相同凭证。
如果提供商密钥存放在单独的网络代理中,应用程序根本不会持有提供商凭证。即使依赖项被破坏,攻击者也只能访问应用程序进程中存在的任何受限凭证。这不能消除风险,但可以减少出现问题时的影响范围。
在本文中,我们将研究两种主要的 LLM 网关架构,精确检查每种设计中 API 密钥的存放位置,演示一个可复现的演示展示实际差异,并讨论为什么减少影响范围通常比试图消除每一种可能的攻击更重要。
每个 LLM 应用程序都有相同的基本工作:向模型提供商发送请求,并使用 API 密钥对该请求进行身份验证。
重要的不是应用程序是否使用 API 密钥。而是在发送请求时该密钥存在的位置。
如今,大多数 AI 应用程序遵循以下两种架构模式之一。
这是大多数开发人员已经熟悉的架构。
你的应用程序从环境变量加载提供商 API 密钥,初始化一个 SDK 或网关库,并直接向 OpenAI、Anthropic、Gemini 或其他提供商发送请求。
简化版本通常如下所示:
api_key = os.environ["PROVIDER_API_KEY"]
client = OpenAI(api_key=api_key)
response = client.responses.create(...)
它易于理解、快速实现,对许多项目都是完全合理的。应用程序拥有凭证,因为它是直接与提供商通信的组件。
重要的细节是提供商密钥现在存放在应用程序的进程中。在该进程中执行的每个包、框架、插件和依赖项都以相同的权限运行。如果其中一个依赖项被破坏,提供商密钥就存在于与恶意代码相同的环境中。
架构问题是这样的:
如果应用程序进程中的某些东西被破坏,有哪些机密可以从那里获得?
第二种模式将身份验证与应用程序本身分离。
应用程序不是直接向模型提供商发送请求,而是将其发送到网关或代理。代理持有提供商 API 密钥,并代表应用程序执行上游请求。
从应用程序的角度来看,流程几乎相同:
Application
│
▼
Gateway / Proxy
│
▼
LLM Provider
区别在于应用程序没有的东西。
应用程序通常不存储提供商凭证,而是持有一个范围内的网关令牌,该令牌授权通过代理的请求。代理验证该令牌,应用任何路由或策略决策,然后在转发上游请求之前仅在其自身进程中注入提供商 API 密钥。
这改变了妥协的后果。如果恶意代码在应用程序进程中执行,它仍然可以访问应用程序拥有的任何凭证。不同的是提供商 API 密钥不再是其中之一。
这不能使应用程序无懈可击。被盗的网关令牌仍然是一个安全事件。但是,与提供商 API 密钥不同,网关令牌可以被严格范围限制、集中撤销、不重新部署应用程序就能轮换,并限制为特定操作。
看到这种差异的最简单方法是观察完全相同的受损依赖项对两种架构的运行。这是我们接下来要做的。
更重要的问题是应用程序启动后会发生什么。
一旦进程开始执行,它需要的凭证就对该进程可用。如果你的应用程序可以读取 API 密钥,任何以相同权限执行的代码都可能也能读取它。
这正是为什么供应链攻击变得如此有效。
攻击者不再需要在你的应用程序中找到漏洞。相反,他们在你的依赖树中的某处破坏一个包,并让你的应用程序代表他们执行有效负载。在许多情况下,该代码在安装或导入期间运行,远早于你自己的业务逻辑启动。
import os
_INTERESTING = ("API_KEY", "SECRET", "TOKEN", "PASSWORD", "PRIVATE_KEY")
def harvest():
found = {
k: v
for k, v in os.environ.items()
if any(marker in k.upper() for marker in _INTERESTING)
}
for name, value in found.items():
print(f"Found: {name}")
harvest()
这个例子刻意设计得是无害的。它不发送网络请求、不写入文件、也不尝试窃取任何东西。它只是扫描当前进程以寻找凭证并打印找到的内容。
重要的部分不是代码做什么。而是代码运行的位置。
想象这个包在你的依赖图中嵌套了好几层。你不直接导入它,也从未读过其源代码。有一天,一个受损的版本到达你的 CI 管道,自动安装,并作为正常启动序列的一部分执行。
如果你的应用程序在其自身环境中存储提供商 API 密钥,依赖项可以读取该密钥,因为它存在于同一进程中。
如果你的应用程序只持有一个范围内的网关令牌,而提供商凭证存放在单独的代理进程中,完全相同的依赖项仍然成功执行,但提供商密钥根本不存在被发现。
这就是我们要探索的架构区别。
这也是为什么 2026 年 3 月的 LiteLLM 供应链事件在整个 AI 生态系统中引起了很多关注。这个事件之所以重要,不是因为 LiteLLM 特别脆弱。而是因为它展示了 AI 基础设施作为攻击目标变得多么有价值,以及受损的依赖项有多快就能到达运行应用程序内的高价值凭证。
在查看现实中的案例之前,值得看看区别。
以下可复现的演示对两种架构运行相同的受损依赖项。依赖项的任何东西都不改变。唯一的变量是提供商 API 密钥存放的位置。
理论是有用的,但当你能看到它发生时,理解架构风险要容易得多。
为了使这个比较具体化,我整理了一个小的、无外部依赖的演示(由 Jonathan Hutchins 为本文提供),它对两种架构重新创建完全相同的场景。
设置有意保持简单:
演示仅使用 Python 的标准库。没有外部服务、没有提供商账户、没有对 OpenAI 或 Anthropic 的网络调用,以及没有真实凭证。一切都在本地运行,使其易于复现而无需担心副作用。
"恶意"依赖项同样简单。当它被导入时,它扫描当前进程以寻找看起来像凭证的任何东西。
import os
_INTERESTING = (
"API_KEY",
"SECRET",
"TOKEN",
"PASSWORD",
"PRIVATE_KEY",
)
def harvest():
found = {
k: v
for k, v in os.environ.items()
if any(marker in k.upper() for marker in _INTERESTING)
}
for name, value in found.items():
shown = value[:8] + "..." if len(value) > 12 else value
print(f"EXFILTRATED {name} = {shown}")
harvest()
第一个版本遵循许多 AI 应用程序今天使用的架构。
应用程序从其自身环境读取提供商密钥:
api_key = os.environ["PROVIDER_API_KEY"]
当依赖项被导入时,它在完全相同的进程中运行。
因此,它立即发现提供商凭证:
[malicious_dep@import]
EXFILTRATED PROVIDER_API_KEY = sk-provi...3xyz
没有什么特别巧妙的事情发生在这里。
这个依赖库并没有绕过身份验证、利用内存腐败或入侵到另一项服务。它只是访问了已经存在于其执行所在进程中的数据。
从攻击者的角度来看,这就足够了。
场景 B:提供商密钥位于网络代理内
现在让我们针对第二种架构运行完全相同的依赖库。
这次,应用程序不会接收提供商凭证。
相反,它只持有一个网关令牌:
token = os.environ["GATEWAY_TOKEN"]
提供商 API 密钥仅存在于代理进程内,该进程在将请求转发上游之前验证网关令牌。
当受损的依赖库运行时,输出看起来非常不同:
[malicious_dep@import]
EXFILTRATED GATEWAY_TOKEN = gw-scope...-789
注意一下没有出现的东西。
没有提供商 API 密钥,因为它从一开始就从未存在于应用程序的进程中。
应用程序仍然收到成功的模型响应,但对 LLM 提供商的身份验证发生在代理内部而非应用程序内部。
到了这个时刻,很容易得出代理"解决"了问题的结论。
依赖库仍然窃取了一个凭证。网关令牌是真实的,如果攻击者获得它,仍然可能通过代理进行请求。否认这一点会让这种对比变得不那么有用。
问题不在于是否有东西泄露。而是泄露了什么、那个凭证能做什么,以及你能多快地从其泄露中恢复。
这正是两种架构开始以更有实质意义的方式产生分歧的地方。
演示的下一部分展示了在网关令牌已被盗后会发生什么,以及为什么恢复过程与轮换被破坏的提供商 API 密钥看起来如此不同。
代理保护什么和不保护什么
在之前的例子中,受损的依赖库仍然窃取了一个凭证。
只是不是提供商 API 密钥。
相反,它获得了一个作用域网关令牌,允许通过代理发起请求。这仍然是一个安全事件,重要的是要提前承认这一点。安全讨论在描述权衡时会变得更加有用。
有趣的部分出现在被破坏之后。演示的 rotate_demo.sh 脚本逐步走过恢复过程。
最初,合法应用程序和攻击者都拥有相同的网关令牌,所以两者都可以成功使用它。这种暂时重叠在操作员撤销被破坏凭证前是可预期的。
然后操作员更新代理的令牌存储。
原始令牌被撤销。
颁发新的作用域令牌。
应用程序代码中没有任何变化。
没有重新部署。
没有重新启动。
代理只是开始拒绝被破坏的凭证,同时接受替代凭证。
结果看起来像这样:
STEP 4 轮换后
[attacker (stolen v1)] BLOCKED -> HTTP 401
[app (v2)] ACCEPTED -> completion ok
演示的最后部分展示了网关令牌的另一个重要特性:作用域。
令牌不是代表对 LLM 提供商账户的无限制访问,而是仅对其被明确创建用于执行的操作有效。
如果在其允许的范围之外呈现该令牌,代理会拒绝它。
STEP 5 作用域
[app (v2, wrong scope)] BLOCKED -> HTTP 403
提供商 API 密钥通常是长期的,可以直接访问你的提供商账户。如果它被破坏,轮换往往意味着在多个服务中更新密钥、重新部署应用程序,并小心协调变更以避免停机。
网关令牌代表的东西小得多。它可以限定于单个应用程序、路由、团队或临时工作负载。如果它泄露,操作员可以集中撤销它、颁发替代凭证,并继续运行而不需要接触提供商凭证本身。这并不能使破坏变得无害,但它使恢复变得更加简单。
随着 AI 系统变得越来越复杂,这种区别变得越来越相关——含有 AI 智能体工作流的系统依赖于许多库、插件、编排框架和 MCP 服务器。每增加一个组件都会扩展信任计算基础,使爆炸范围缩减和完全防止故障一样重要。
当然,这不仅仅是理论讨论。2026 年 3 月,AI 生态见证了一场真实的供应链破坏事件,恰好说明了为什么凭证的位置如此重要。与其让开发者想象风险,它提供了一个真实案例,说明被破坏的依赖库能多快转变成更大的安全事件。这正是我们接下来要分析的事件。
2026 年 3 月 LiteLLM 供应链攻击详解
2026 年 3 月,LiteLLM 是与多个 LLM 提供商交互的最广泛使用的网关之一,它成为了一场影响多个开源项目的更大软件供应链活动的一部分。
根据 LiteLLM 自己的安全事后分析,攻击者在从项目的 CI 管道窃取发布令牌后,能够将两个被破坏的包版本(1.82.7 和 1.82.8)发布到 PyPI。破坏本身源于上游的恶意 GitHub Action,而非 LiteLLM 应用程序代码中的漏洞,这一细节也由 Datadog 安全实验室和 FutureSearch 记录。
LiteLLM 的事后分析估计恶意版本可用约 40 分钟,而独立分析将时间窗口估为接近三小时。无论如何,这都足以让自动化 CI 管道安装被破坏的包。
恶意版本搜索了高价值凭证,包括云密钥、SSH 密钥、Kubernetes 令牌、数据库凭证和 API 密钥,然后试图将其泄露。LiteLLM 的事后分析提供了受影响凭证类型的详细列表,Datadog 安全实验室则分析了有效载荷执行后的运作方式。
最公开的下游受害者之一是 Mercor,其后来确认了与被破坏包相关的安全事件。这个案例说明了广泛使用的依赖库中的破坏如何能快速传播到从未与原始攻击者直接交互的组织。
要点并不是 LiteLLM 特别有风险。破坏源于恶意的 GitHub Action 而非 LiteLLM 的应用程序代码,项目通过发布事后分析、重建发布管道并发布清洁版本(v1.83.0)快速作出响应。官方 LiteLLM Proxy Docker 部署固定了依赖项,也未受影响,这强化了依赖项固定、锁定文件和已验证构建的价值。
最重要的教训与架构有关。
LLM 网关占据了一个独特的敏感位置,因为它们管理解锁多个提供商访问权限的凭证。这些凭证所在的任何地方在被破坏时都会成为有吸引力的目标。
这就是为什么问题不是"我的某个依赖库明天会被破坏吗?"
而是:如果这种情况明天发生,攻击者会在我的应用程序进程内找到什么凭证?
那么,你应该在哪里存放 LLM API 密钥?
答案不是"总是在代理后面"或"总是在应用程序内部"。
正确的架构取决于你的团队的运维需求、部署模型、性能要求和安全优先级。
希望本文能阐明的一点是,提供商 API 密钥的位置直接决定了破坏的后果。
如果应用程序持有提供商密钥,任何以应用程序特权执行的代码都可能访问它。这并不会自动使架构不安全。许多生产系统通过固定的依赖项、锁定文件、隔离的 CI/CD 管道、密钥管理器和严格的网络控制,成功地使用进程内库。
如果应用程序改为与网络代理通信,提供商密钥会移到单独的进程中。应用程序通常只持有一个作用域网关令牌。如果恶意代码在应用程序内执行,攻击者仍然可以窃取该令牌,但提供商账户本身保持在应用程序的爆炸范围之外。恢复变成了撤销和轮换作用域凭证的事情,而不是替换依赖它的每个服务中的提供商密钥。
两种架构都不能消除对依赖项固定、可重现构建、CI/CD 加固、最小特权访问和持续监控的需求。无论你的 API 密钥位于何处,这些实践都保持必要。架构仅决定了如果这些防御失效,攻击者能到达什么。
如果你正在评估自己的架构,几个实际问题可以帮助指导讨论:
我的应用程序运行时,提供商 API 密钥存在于哪里?
如果我的应用程序中的某个依赖项今天遭到攻击,它能够访问什么机密?
这些凭证能否独立于提供商账户进行限制范围、撤销和轮换?
凭证泄露后恢复需要多长时间?
如果你考虑采用基于代理的架构,目前有多种实现方案可供选择,包括官方的 LiteLLM Proxy 部署、放在模型提供商前面的云 API 网关,以及 SteadIO 等原生代理解决方案。
SteadIO 是一个开源自托管的 LLM 网关,位于应用程序和 OpenAI、Anthropic 等模型提供商之间。除了将提供商 API 密钥与应用程序进程隔离外,它还添加了多项操作功能,这些功能随着 AI 系统的增长变得越来越有价值。
关键功能包括:
Per-agent 和 Per-team 请求归属,可轻松了解哪些智能体在生成流量。
使用提供商准确定价的实时令牌和成本跟踪。
预算强制执行,允许团队在成本失控前自动停止失控的智能体。
集中式身份验证和网关令牌管理,因此可以在不更改提供商 API 密钥的情况下发行、撤销和轮换作用域凭证。
用于监控 AI 流量的单一控制平面,因为每个请求都已通过网关。
无论您选择哪个网关,关键的架构优势是通过将提供商凭证保持在应用程序进程之外来减少爆炸半径。
归根结底,问题并不真正关于 API 密钥。而是关于设计能够优雅失败的系统。
因为无论您的安全计划变得多么成熟,漏洞会出现,依赖关系会遭到攻击,错误会发生。当那一天来临时,最有价值的安全决策可能不是阻止事件发生的决策。
它可能是使爆炸半径足够小以便快速恢复的架构决策。
没有任何架构能够防止所有供应链攻击或被攻击的依赖关系。您可以控制的是当出现问题时暴露哪些凭证,以及您能多快地恢复。
因此,在发布下一个 AI 应用程序之前,花点时间回答我们开始时的问题:
您的 LLM API 密钥实际上位于哪里?
答案可能对您的安全态势的影响比您选择的模型提供商或 SDK 更大。
本文由 SteadIO 创始人 Jonathan Hutchins 共同撰写,他的技术见解和演示帮助塑造了本文中探讨的许多架构概念。
Hadil Ben Abdallah Follow
某些评论可能仅对已登录的访问者可见。登录以查看所有评论。
如需进一步行动,您可以考虑屏蔽此人和/或举报滥用行为