AI 生成的应用往往需要代表用户向外部服务进行身份验证。这就带来一个问题:生成的代码不应该接触到用户的凭据。
我们在构建 v0 的 Snowflake 集成时就面临这样的抉择。该集成允许用户连接 Snowflake、查看 schema、查询数据,并生成直接在其数据仓库上运行的应用。这些生成的代码必须向 Snowflake 进行身份验证,但它由模型编写、运行时没有人工审查,而且提示词注入可能诱导它外泄一切它能读取到的内容,因此用户的 OAuth 令牌绝不应进入代码运行的环境。
我们的解决方案是为 v0 沙箱搭建一个 Snowflake 请求代理,它构建在 Vercel Sandbox 防火墙之上。沙箱里可以运行常规的 Snowflake 客户端,而真正的凭据则在沙箱运行时之外的服务端代理中、于请求发出时才解析。这样一来,现有的 Snowflake 客户端无需改动即可在沙箱内工作,用户的凭据也不会暴露给生成的代码。
更难的问题是确定代理可以在哪里安全地注入凭据。最直观的实现是在占位符令牌出现的任何位置都将其替换,但这会引入新的凭据泄露风险。
隔离并不能保护机密
v0 在隔离的沙箱中运行生成的应用。隔离可以保护系统其余部分免受不可信代码的影响,但无法保护沙箱内部的机密。
如果生成的应用能从文件系统中读取令牌,这个令牌就可能被复制到日志里、出现在 API 响应中、被嵌入生成的客户端代码,或者被发送到其他主机。沙箱隔离限制了应用能访问的范围,但一旦凭据本身就在沙箱内可用,隔离也就无济于事了。
对 Snowflake 而言,凭据代表用户所连接的 Snowflake 角色。v0 应当能帮助用户探索并利用其有权访问的数据进行构建,但生成的代码不应仅仅因为需要凭据,就拿到原始的服务商凭据。
每个 Snowflake 请求都经过代理
沙箱无法直接与 Snowflake 通信。当沙箱内的代码向用户的 Snowflake 账户主机发送请求时,沙箱防火墙会把该请求转发给 v0 Snowflake 代理:


从生成的代码到 Snowflake 的请求路径。代理运行在沙箱之外。
防火墙使用每个沙箱专属的证书颁发机构终止 TLS,这样代理就能读取并改写原本加密的流量。沙箱会自动信任该证书颁发机构,因此 Snowflake SDK 和 CLI 可以按默认的证书验证(包括 OCSP)运行。随后,代理会验证沙箱的 OIDC 令牌,查找该沙箱所属的 v0 聊天,恢复与该聊天绑定的用户会话,并为该用户取回一个新的 Snowflake 凭据。
network-policy/index.ts
return {
[host]: [
// absolute URL, e.g. https://v0.app/chat/api/sandbox-proxy/snowflake
{ forwardURL: getSandboxRequestProxyForwardURL('snowflake') },
],
}
由 v0 内部转发配置简化而来
沙箱无权决定携带令牌的请求发往何处。代理会根据服务端凭据推导出 Snowflake 账户主机,并拒绝无效的账户 URL,使凭据始终限定在所连接的 Snowflake 账户范围内,而不是信任生成代码提供的主机信息。
兼容而不暴露凭据
作为权宜之计,集成的第一个版本把用户的真实令牌写入了沙箱的 Snowflake 令牌文件。代理取代了这种做法。理想情况下,我们会彻底移除沙箱中的令牌文件,但 Snowflake 客户端会根据不同的身份验证流程,到不同的位置去查找凭据。
有些请求,例如 Snowflake SQL API 调用,通过 Authorization: Bearer 请求头进行身份验证。其他客户端流程则读取本地令牌文件,并把令牌作为登录请求的一部分发送。客户端完成身份验证后,后续请求使用 Snowflake 签发的会话令牌,代理对这些请求原样放行。
为了保持兼容,v0 仍会在沙箱中写入一个形似令牌的占位符。该占位符是一个固定的、公开的 72 字节字符串,不授予任何访问权限,它唯一的作用是让现有的 Snowflake SDK 和 CLI 流程表现得好像令牌确实存在。代理从不依据占位符本身来授权请求,而是依据沙箱在服务端的身份及其与用户聊天的绑定关系来授权。
真正的 OAuth 令牌,也就是代表用户、可重复使用的凭据,从不会写入沙箱。登录之后,Snowflake 确实会签发存在于沙箱内的会话令牌,但每个会话令牌都只属于单个已认证的会话。
这些会话在实际中存活时间很短。生成的应用中的 Snowflake 辅助函数会在每次查询后销毁其连接,会话随即结束。关闭聊天本身并不会结束会话,但销毁沙箱会把令牌一并带走,而且 Snowflake 默认会在会话闲置四小时后于服务端使其过期。请求到达代理后,由代理决定是否附加凭据以及附加到哪里。
为什么盲目替换行不通
代理的第一个版本会在每个请求中搜索占位符,并在其出现的任何位置换成真实令牌:
network-policy/snowflake-proxy.ts
const text = await request.text();
const patched = text.replaceAll(placeholder, realToken);
request = new Request(request, { body: patched });
盲目替换会在请求体中占位符出现的任何位置打补丁。
问题在于,生成的代码能够控制请求的某些部分,包括占位符可以出现在哪里。例如,SQL 语句就是调用方可控的数据。如果查询中把占位符作为字符串字面量,盲目替换就会把这条查询变成包含真实 OAuth 令牌的查询。如果数据库随后把该字符串返回,真实令牌就会作为查询结果回到沙箱里,代理也就泄露了它本应挡在沙箱之外的令牌。
更严格的匹配也堵不住这个漏洞。代理不应基于攻击者可控的文本来注入凭据,而是需要知道每个 Snowflake 请求中究竟哪个字段承载身份验证信息。
只在身份验证字段中注入凭据
Snowflake 请求以三种形态到达代理,每种形态把身份验证放在不同的位置。
对于 Snowflake SQL API 请求,代理把 OAuth 令牌设置在 Authorization: Bearer 请求头上。请求体仍是调用方可控的 SQL,不会被改写。如果占位符出现在 SQL 负载中,代理会在请求抵达 Snowflake 之前将其拒绝,并记录为占位符滥用。
对于 Snowflake 登录请求,代理会解析 JSON 请求体,在登录令牌字段处以结构化方式设置令牌,然后重新序列化请求体。如果占位符仍残留在请求体的其他任何位置,代理会以失败关闭(fail closed)的方式处理。
登录后的会话请求使用 Snowflake 管理的会话令牌进行身份验证,因此代理无需注入任何内容。
失败关闭
出现以下任一情况,代理都会拒绝请求:
- 沙箱未绑定到某个聊天
- 无法获得用户范围的凭据
- 无法推导出 Snowflake 账户主机
- 占位符出现在获准的身份验证字段之外
- 无法安全地解析结构化的登录请求体
请求在检查之前还会受到大小限制,避免压缩过的或过大的请求体让代理沦为无上限的解析器。每个经过代理的请求都会产生一条可观测性事件,记录上游结果、状态、耗时和注入位置。这样既能暴露滥用行为和集成故障,又不会泄露机密。
处理刷新与部署
令牌刷新不能依赖环境中的浏览器 cookie,因为代理请求来自沙箱,而不是用户的浏览器会话。v0 转而把用户会话绑定到沙箱,由代理基于这一绑定来签发并刷新 OAuth 凭据。
发布路径使用 Snowflake CLI 流程,并遵循同样的凭据边界。部署时可以把占位符写入凭据文件,但绝不会写入真实令牌。
部署之后,应用运行在 Snowpark Container Services 中,并以其自身的服务用户身份进行身份验证,所用令牌由 Snowflake 管理并自动轮换,挂载在 /snowflake/session/token。用户的 OAuth 令牌和 v0 代理都不再参与其中。
最终形成的安全边界
对用户来说,这一切都是不可见的。连接 Snowflake,让 v0 查看 schema 或构建应用,预览结果,准备好后再部署即可。
该安全模型建立在五条规则之上:
- 凭据只在各端点的身份验证字段中注入
- 在身份验证字段之外使用占位符的请求会被拒绝并记录
- 携带令牌的请求只会发往所连接的 Snowflake 账户主机
- 凭据在服务端签发和刷新
- 生成的代码使用 Snowflake 时,从不会读取用户的 OAuth 令牌
在上线后的前 15 天里,代理在服务端为大约 13,000 个请求附加了凭据,并且记录到零次占位符滥用拒绝。
虽然我们是为 Snowflake 构建了这个代理,但同样的问题也存在于其他集成中:生成的代码往往需要在拿不到用户长期凭据的情况下完成身份验证。关键在于只把凭据注入协议定义的身份验证字段,而不是改写任意的请求数据。
v0 Snowflake 集成现已进入 beta 阶段。阅读集成文档即可开始使用。
