Vercel详解v0如何在AI生成代码场景下通过服务端代理实现Snowflake身份验证,避免用户OAuth Token进入沙箱环境,有效防御提示词注入攻击。
AI 生成的应用通常需要代表用户向外部服务进行身份验证。这带来一个问题:生成的代码不应该访问用户的凭证。
在构建 v0 Snowflake 集成时我们就面临了这个抉择。它让用户连接 Snowflake、检查 schema、查询数据,并生成针对其数据仓库运行的应用。生成的代码必须向 Snowflake 进行身份验证,但它是由模型生成的,运行起来没有人工审查,而且提示注入可以引导它泄露它能读取的任何内容,因此用户的 OAuth token 不应该进入代码运行的環境。
我们通过为 v0 沙箱构建基于 Vercel Sandbox 防火墙的 Snowflake 请求代理解决了这个问题。沙箱可以运行正常的 Snowflake 客户端,但真正的凭证是在沙箱运行环境之外的服务器代理中在请求时解析的。这使得现有的 Snowflake 客户端可以在沙箱内工作,而不会将用户的凭证暴露给生成的代码。
更难的问题是确定代理可以在哪里安全地注入凭证。显而易见的实现方式——替换出现的占位符 token——会引入另一个凭证泄露。
v0 在隔离的沙箱中运行生成的应用程序。隔离保护系统其余部分免受不可信代码的侵害,但不能保护沙箱内部的 secrets。
如果生成的应用程序可以从文件系统读取 token,那么该 token 就可以被复制到日志中、在 API 响应中返回、嵌入到生成的客户端代码中,或发送到另一台主机。沙箱隔离限制了应用程序可以访问的内容,但一旦凭证本身在沙箱内可用,它就无能为力了。
对于 Snowflake,凭证代表了用户连接的 Snowflake 角色。v0 应该能够帮助用户探索和构建他们有权访问的数据,但生成的代码不应该仅仅因为需要一个凭证就接收原始的提供者凭证。
沙箱不能直接与 Snowflake 通信。当其中的代码向用户的 Snowflake 账户主机发送请求时,沙箱防火墙会将该请求转发到 v0 Snowflake 代理:
防火墙使用对每个沙箱唯一的证书颁发机构终止 TLS,这使得代理可以读取和重写本来会被加密的流量。沙箱自动信任该证书颁发机构,因此 Snowflake SDK 和 CLI 使用其默认证书验证(包括 OCSP)运行。然后代理验证沙箱的 OIDC token,查找沙箱所属的 v0 对话,恢复绑定到该对话的用户会话,并为该用户获取一个新的 Snowflake 凭证。
1return {
2 [host]: [
3 // absolute URL, e.g. https://v0.app/chat/api/sandbox-proxy/snowflake
4 { forwardURL: getSandboxRequestProxyForwardURL('snowflake') },
5 ],
6}
简化自内部 v0 转发配置
沙箱不选择携带 token 的请求被发送到哪里。代理从服务器端凭证派生 Snowflake 账户主机,并拒绝无效的账户 URL,将凭证限制在连接的 Snowflake 账户,而不是信任生成代码提供的主机信息。
作为权宜之计,集成版本将用户的真实 token 写入了沙箱的 Snowflake token 文件。代理取代了这种方法。理想情况下,我们会从沙箱中完全移除 token 文件,但 Snowflake 客户端根据身份验证流程的不同,期望凭证位于不同的位置。
某些请求(如 Snowflake SQL API 调用)使用 Authorization: Bearer 头进行身份验证。其他客户端流程读取本地 token 文件,并将 token 作为登录请求的一部分发送。一旦客户端通过身份验证,后续请求使用 Snowflake 颁发的 session token,代理会原样传递这些请求。
为了保持兼容性,v0 仍然将一个 token 形状的占位符写入沙箱。该占位符是一个固定的、公开的 72 字节字符串,不授予任何访问权限,它的唯一作用是让现有的 Snowflake SDK 和 CLI 流程表现得好像存在一个 token。代理永远不会基于占位符本身来授权请求。相反,代理使用沙箱的服务器端身份及其与用户对话的绑定来授权请求。
真正的 OAuth token——代表用户的可重用凭证——永远不会写入沙箱。Snowflake 确实会在登录后在沙箱内颁发 session token,但这些 token 每一个都属于一个经过身份验证的会话。
实际上这些会话的寿命很短。生成应用中 Snowflake 帮助程序在每次查询后销毁其连接,这会立即结束会话。关闭聊天不会单独结束会话,但拆除沙箱会带走 token,而 Snowflake 默认会在四小时不活动后服务器端使会话过期。当请求到达代理时,它决定是否以及在哪里附加凭证。
代理的第一个版本在每个请求中搜索占位符,并将其出现的任何地方替换为真实的 token:
1const text = await request.text();
2const patched = text.replaceAll(placeholder, realToken);
3request = new Request(request, { body: patched });
盲目替换会修补请求体中任何出现占位符的地方。
问题是生成的代码控制了请求的某些部分,包括占位符可以出现的位置。例如,SQL 语句是调用者控制的数据。如果查询包含作为字符串字面量的占位符,盲目替换会将该查询转换为包含真实 OAuth token 的查询。如果数据库随后返回该字符串,真实的 token 就会作为查询输出回到沙箱,代理就会泄露它本来要阻止的 token。
更严格的匹配也无法关闭这个漏洞。代理不应该基于攻击者控制的文本注入凭证。相反,它需要知道每个 Snowflake 请求中哪个字段携带身份验证信息。
Snowflake 请求以三种形式到达代理,每种形式的身份验证位于不同的位置。
对于 Snowflake SQL API 请求,代理将 OAuth token 设置在 Authorization: Bearer 头上。body 仍然是调用者控制的 SQL,不会被重写。如果占位符出现在 SQL payload 中,代理会在请求到达 Snowflake 之前拒绝它,并将其记录为占位符滥用。
对于 Snowflake 登录请求,代理解析 JSON 请求 body,在登录 token 字段上以结构化方式设置 token,然后重新序列化 body。如果占位符仍然出现在请求 body 的其他任何地方,代理会失败关闭。
登录后的会话请求使用 Snowflake 管理的 session token 进行身份验证,因此代理无需注入任何内容。
代理拒绝任何满足以下条件的请求:
沙箱未绑定到对话
沙箱未绑定到对话
无法获取用户范围的凭证
无法获取用户范围的凭证
无法派生 Snowflake 账户主机
无法派生 Snowflake 账户主机
占位符出现在经批准的身份验证字段之外
占位符出现在经批准的身份验证字段之外
无法安全解析结构化登录 body
无法安全解析结构化登录 body
请求在检查之前就有大小限制,这样压缩或过大的 body 就不会将代理变成无限制的解析器。每个代理的请求都会发出一个可观察性事件,包含上游结果、状态、持续时间和注入位置。这在不会暴露 secrets 的情况下展示滥用和集成失败。
Token 刷新不能依赖环境浏览器 cookie,因为代理请求源自沙箱,而不是用户的浏览器会话。v0 将用户会话绑定到沙箱,代理从该绑定中 mint 和刷新 OAuth 凭证。
发布路径使用具有相同凭证边界的 Snowflake CLI 流程。部署可以将占位符写入凭证文件,但永远不会写入真实的 token。
部署后,应用程序在 Snowpark Container Services 中运行,并作为自己的服务用户进行身份验证,使用 Snowflake 管理并自动轮换的 token,挂载在 /snowflake/session/token。用户的 OAuth token 和 v0 代理不再参与。
对于用户来说,这一切都是不可见的。连接 Snowflake,让 v0 检查 schema 或构建应用,预览结果,准备好了就部署。
安全模型基于五条规则:
凭证注入仅在每个端点的身份验证字段中进行
凭证注入仅在每个端点的身份验证字段中进行
在身份验证字段之外使用占位符的请求会被拒绝并记录
在身份验证字段之外使用占位符的请求会被拒绝并记录
携带 token 的请求仅发送到连接的 Snowflake 账户主机
携带 token 的请求仅发送到连接的 Snowflake 账户主机
凭证在服务器端 mint 和刷新
凭证在服务器端 mint 和刷新
生成的代码使用 Snowflake 但从不读取用户的 OAuth token
生成的代码使用 Snowflake 但从不读取用户的 OAuth token
在投入生产的前 15 天,代理为大约 13,000 个请求附加了服务器端凭证,记录了零次占位符滥用拒绝。
虽然我们为 Snowflake 构建了这个代理,但其他集成也面临同样的问题:生成的代码通常需要在不接收用户长期凭证的情况下进行身份验证。重要的是只将凭证注入到协议定义的身份验证字段中,而不是重写任意请求数据。
v0 Snowflake 集成现已推出 beta 版。开始阅读集成文档。