AI 生成的应用通常需要代表其用户向外部服务进行身份验证。这就带来了一个问题:生成的代码不应有权访问用户的凭据。
在构建 v0 Snowflake 集成时,我们面临了这一抉择。它允许用户连接 Snowflake、检查 schema、查询数据,并生成针对其数据仓库运行的应用程序。生成的代码必须向 Snowflake 进行身份验证,但它由模型编写并在无人工审查的情况下运行,而且提示注入可以引导它窃取任何它能读取的内容,因此用户的 OAuth 令牌绝不应进入代码运行的环境。
我们通过为 v0 沙箱构建的 Snowflake 请求代理解决了这一问题,它基于 Vercel Sandbox 防火墙。沙箱可以运行普通的 Snowflake 客户端,但真正的凭据会在请求时于沙箱运行时之外的服务器代理中解析。这使得现有的 Snowflake 客户端可以在沙箱内正常工作,而不会将用户凭据暴露给生成的代码。
更难的问题在于确定代理可以在何处安全地注入凭据。显而易见的实现方式——替换占位符令牌出现的任何位置——会引入另一种凭据泄露。
复制标题链接隔离并不能保护机密
v0 在隔离的沙箱中运行生成的应用程序。隔离保护系统的其余部分免受不受信任代码的影响,但它并不能保护沙箱内部的机密。
如果生成的应用程序可以从文件系统读取令牌,那么该令牌就可以被复制到日志中、在 API 响应中返回、嵌入到生成的客户端代码中,或发送到另一台主机。沙箱隔离限制了应用程序可以访问的内容,但一旦凭据本身在沙箱内可用,它就无济于事。
对于 Snowflake 而言,凭据代表用户连接时使用的 Snowflake 角色。v0 应当能够帮助用户探索并使用他们有权访问的数据进行构建,但生成的代码不应仅仅因为需要一个凭据就获得原始的提供商凭据。
复制标题链接每个 Snowflake 请求都经过代理
沙箱无法直接与 Snowflake 通信。当其中的代码向用户的 Snowflake 账户主机发送请求时,沙箱防火墙会将该请求转发到 v0 Snowflake 代理:


防火墙使用每个沙箱独有的证书颁发机构来终止 TLS,这使得代理能够读取并重写原本会被加密的流量。沙箱自动信任该证书颁发机构,因此 Snowflake SDK 和 CLI 在默认证书验证(包括 OCSP)下运行。随后,代理验证沙箱的 OIDC 令牌,查找沙箱所属的 v0 聊天,恢复绑定到该聊天的用户会话,并为该用户获取新的 Snowflake 凭证。
return { [host]: [ // 绝对 URL,例如 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 默认在四小时不活动后会在服务器端使会话过期。当请求到达代理时,代理决定是否以及在哪里附加凭证。
复制标题链接为什么盲目替换会失败
代理的最初版本在每个请求中搜索占位符,并在其出现的任何位置替换为真实令牌:
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 请求体,在登录令牌字段以结构化方式设置令牌,然后再次序列化请求体。如果占位符仍留在请求体的其他任何位置,代理会故障关闭。
登录后的会话请求使用 Snowflake 管理的会话令牌进行身份验证,因此代理无需注入任何内容。
复制标题链接故障关闭
代理会拒绝以下任何请求:
沙箱未绑定到聊天
无法获取用户范围的凭据
无法推导出 Snowflake 账户主机
占位符出现在已批准的身份验证字段之外
无法安全解析结构化登录请求体
请求在检查前也会受到大小限制,这样压缩或超大请求体就不会把代理变成无界解析器。每个被代理的请求都会发出一个可观测性事件,包含上游结果、状态、持续时间和注入位置。这可以在不暴露机密的情况下暴露误用和集成故障。
复制标题链接处理刷新和部署
令牌刷新不能依赖环境中的浏览器 cookie,因为代理请求源自沙箱,而不是用户的浏览器会话。v0 改为将用户会话绑定到沙箱,代理根据该绑定生成并刷新 OAuth 凭据。
发布路径使用 Snowflake CLI 流程,并遵循相同的凭据边界。部署可以将占位符写入凭据文件,但绝不会写入真实令牌。
部署后,应用程序在 Snowpark Container Services 中运行,并以其自己的服务用户身份进行身份验证,使用由 Snowflake 管理并自动轮换的令牌,挂载在 /snowflake/session/token
。用户的 OAuth 令牌和 v0 代理不再参与。
复制标题链接由此产生的安全边界
对用户来说,这一切都不可见。连接 Snowflake,让 v0 检查模式或构建应用,预览结果,并在准备好时部署。
安全模型基于五条规则:
凭据注入仅发生在每个端点的身份验证字段
在身份验证字段之外使用占位符的请求会被拒绝并记录日志
携带令牌的请求仅发往已连接的 Snowflake 账户主机
凭据在服务端生成和刷新
生成的代码使用 Snowflake 时从不读取用户的 OAuth 令牌
在生产环境的前 15 天里,该代理在服务端为大约 13,000 个请求附加了凭据,并记录了零次占位符误用拒绝。
尽管我们是为 Snowflake 构建了这个代理,但同样的问题也适用于其他集成:生成的代码通常需要在接收不到用户长期凭据的情况下进行身份验证。关键在于仅将凭据注入协议定义的身份验证字段,而不是重写任意请求数据。
v0 Snowflake 集成现已推出测试版。阅读集成文档即可开始使用。
