为用户生成内容平台构建“零差距”秘密
大多数允许用户部署代码的平台处理机密的方式都是一样的:把它们放在环境变量中,然后祝大家好运。我正在构建一个用于发布和重混可运行网页应用的小型社交平台(Vibecodr),当人们能够部署服务器端代码时,第一个请求是可以预见的:“让我用我的秘密调用一个外部API。”
环境变量的方法一直让我感到困扰。一旦你将明文交给用户代码,它就会存在于内存中,可能被记录、意外地在响应中回显,或者通过某个你没有审计的依赖悄悄泄露。当出现问题时,祝你好运找出秘密泄露的地方。
因此,我尝试设计一个更严格的约束:明文秘密绝不应存在于用户的工作内存中。无论是作为环境变量、帮助函数的返回值,还是在日志中。理想情况下,甚至连毫秒都不应存在。
以下是它的工作原理。
**调度层**
一个由平台控制的代理位于用户代码和外部世界之间。用户代码不会直接获取秘密。相反,它调用一个像fetch-with-secret的包装器,并传递:
- 使用哪个秘密(密钥名称,而不是值)
- 出站请求的详细信息
- 在哪里注入秘密(头部、查询参数、正文)
调度层负责敏感的工作:在服务器端解密秘密,验证出站URL(仅限HTTPS、可选的每个秘密的白名单、基础设施主机阻止、基于DNS的SSRF检查),以严格的超时发起上游请求,手动处理重定向并在每个跳转时重新验证,从文本响应中删除秘密,并强制执行配额。
**不透明令牌模式**
显式的fetch-with-secret API是安全的,但有时会显得笨拙——开发者喜欢正常地组合请求。因此还有第二种模式:用户代码请求一个短期的不透明令牌,用于给定的秘密密钥(仍然从未看到实际值)。如果该令牌出现在fetch的URL、头部或正文中,包装器会拦截它并通过调度进行真实的注入。令牌是每个请求的、短期的,只有在同一请求上下文中铸造的情况下才会解析。其他情况则会失败并关闭。
**密钥管理**
我对加密秘密负载格式进行版本控制,以便未来的迁移和密钥轮换不会产生歧义。支持多个候选解密密钥同时存在——可以在不破坏现有加密值的情况下进行轮换,并保持跨组件共享的加密逻辑,以避免实现之间的漂移。
**我实际遇到的陷阱**
重定向是一个安全陷阱。大多数HTTP客户端默认会跟随重定向,这可能会悄悄地将“允许的主机”转变为重定向到内部IP。每个跳转都必须手动处理重定向并重新验证,这是不可谈判的。如果我发布了天真的版本,这将会给我带来严重的问题。
删除机密比听起来要复杂得多。秘密可能以原始、URL编码或base64编码的形式出现在响应中。你希望实现深度防御,而不是让每个响应变得一团糟。短秘密会导致误报的删除——我选择了删除并发出警告,而不是跳过它们,因为“哎呀,泄露了”比“哎呀,损坏了”要糟糕得多。
你需要对所有内容设定严格的上限。请求正文大小、扫描的响应正文大小、超时上限。没有它们,每个“有用的功能”都可能成为拒绝服务攻击的向量。
**我希望得到的反馈**
我发布这个是因为我希望得到那些构建过类似系统或发现其漏洞的人的批评:
- 你在安全处理二进制响应时,注入秘密的请求有什么首选策略?
- 你是否在边缘/无服务器环境中见过与基于DNS的SSRF验证相关的失败模式?
- 对于删除机密与如果可能包含秘密就完全阻止响应,你有什么强烈的看法?
如果你想在真实产品上下文中看到这个项目,可以访问 https://Vibecodr.space——但我在这里是为了获取关于架构和威胁模型的反馈,而不是进行发布。
查看原文
Most platforms that let users deploy code handle secrets the same way: stuff them in an env var and wish everyone luck. I'm building a small social platform for publishing and remixing runnable web apps (Vibecodr), and the moment people could deploy server-side code, the first request was predictable: "let me call an external API with my secret."
The env var approach has always bugged me. The instant you hand plaintext to user code, it lives in memory where it can be logged, accidentally echoed in a response, or quietly exfiltrated through some dependency you didn't audit. When something goes wrong, good luck figuring out where the secret leaked.
So I tried to design around a stricter invariant: the plaintext secret should never exist inside the user's worker memory. Not as an env var, not as a return value from a helper, not in logs. Ideally, not even for a millisecond.
Here's how it works.
The dispatch layer
A platform-controlled proxy sits between user code and the outside world. User code never gets secrets directly. Instead it calls a wrapper like fetch-with-secret, passing:<p>which secret to use (a key name, not the value)
the outbound request details
where to inject the secret (header, query param, body)<p>The dispatch layer does the sensitive work: decrypts the secret server-side, validates the outbound URL (HTTPS-only, optional per-secret allowlist, infrastructure host blocking, DNS-based SSRF checks), makes the upstream request with strict timeouts, manually handles redirects with re-validation at every hop, redacts the secret from text responses, and enforces quotas.
The opaque token mode
The explicit fetch-with-secret API is secure but sometimes awkward—developers like composing requests normally. So there's a second mode: user code requests a short-lived opaque token for a given secret key (still never seeing the actual value). If that token appears in a fetch URL, header, or body, the wrapper intercepts it and routes the request through dispatch for real injection. Tokens are per-request, short-lived, and only resolve if they were minted in the same request context. Anything else fails closed.
Key management
I version the encrypted secret payload format so future migrations and key rotations aren't ambiguous. Multiple candidate decryption keys are supported simultaneously—rotate without breaking existing encrypted values, and keep the crypto logic shared across components so implementations don't drift apart.
The footguns I actually hit
Redirects are a security trap. Most HTTP clients follow redirects by default, which can silently turn an "allowed host" into a redirect to an internal IP. Manual redirect handling with re-validation at every hop was non-negotiable. This one would've bitten me badly if I'd shipped the naive version.
Redaction is trickier than it sounds. Secrets can appear raw, URL-encoded, or base64-encoded in responses. You want defense-in-depth without turning every response into garbage. Short secrets create false-positive redactions—I chose to redact anyway and warn rather than skip them, because "oops, leaked" is worse than "oops, mangled."
You need hard caps on everything. Request body size, response body size for scanning, timeout ceilings. Without them, every "helpful feature" becomes a DoS vector.
Where I'd love pushback
I'm posting this because I want critique from people who've built similar systems or poked holes in them:<p>What's your preferred strategy for handling binary responses safely when secrets have been injected into the request?
Have you seen failure modes around DNS-based SSRF validation in edge/serverless environments specifically?
Any strong opinions on redaction vs. just blocking the response entirely if it might contain the secret?<p>If you want to see this in a real product context, the project is https://Vibecodr.space —but I'm here for feedback on the architecture and threat model, not to do a launch post.