AI 编码助手中的认证检查代码,当 INTERNAL_API_TOKEN 未设置时,undefined === undefined 为 true,所有匿名请求直接放行,造成认证失效。
一个使用 === 做校验、但两边都可能是 undefined 的认证检查是 fail-open 的——一个缺失的配置把它从锁定状态变成了开放状态。
.env.example 里缺少的那个 secret,是 internal auth token。部署时没设它,undefined === undefined 就让所有匿名请求通过了。
默认拒绝:校验类型、拒绝空值、用常量时间比较,并写上那个不携带凭证且环境变量未设置的测试。
coding agent 给了我一段 auth middleware。它通过了 review。我差点就 ship 了。
function protect(req, res, next) {
if (req.headers['x-internal-token'] === process.env.INTERNAL_API_TOKEN) return next();
return requireAuth()(req, res, next);
}
四行代码,看起来没问题。内部服务带着共享 token 调用,跳过登录流程;其他请求都走到真正的认证。然后我检查了 token 没设置时会发生什么。
INTERNAL_API_TOKEN 是 .env.example 里唯一缺失的那个 secret。其他所有 key 都在——Stripe、Clerk、Paddle——唯独没这个。所以在一次没人想到要去设置它的部署中,process.env.INTERNAL_API_TOKEN 是 undefined。
现在一个正常的浏览器请求进来了。它没有发送 x-internal-token 头,所以 req.headers['x-internal-token'] 也是 undefined。
undefined === undefined → true → return next()。
认证被绕过了。所有到 /checkout 的匿名请求都被当作可信的内部调用。token 设好时端点最安全,token 缺失时完全开放——这恰好是反过来的。一个配错的认证检查应该变得更严格,而不是更松。
而且即使 token 已设置,还有一个更隐蔽的问题:=== 比较 secret 不是常量时间的。它在第一个不匹配的字节就短路了,所以响应时间会逐字符泄漏 token。
const crypto = require('crypto');
function hasInternalToken(req) {
const expected = process.env.INTERNAL_API_TOKEN;
if (typeof expected !== 'string' || expected.length === 0) return false; // no token configured → deny
const provided = req.headers['x-internal-token'];
if (typeof provided !== 'string' || provided.length === 0) return false;
const a = Buffer.from(provided), b = Buffer.from(expected);
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
function protect(req, res, next) {
if (hasInternalToken(req)) return next();
return requireAuth()(req, res, next);
}
没有配置 token 就不会 bypass。空的或缺失的 header 不会 bypass比较是常量时间的,且先检查了长度(timingSafeEqual 在长度不匹配时会抛错)。然后把 INTERNAL_API_TOKEN 加到 .env.example 里,这样它就永远不会被悄悄遗漏了。
happy path 看起来是对的。token 设好了,调用方发了正确的,x === y 是 true——review 看到一个似是而非的认证检查就过了。模型优化的是"配置好了就能用",而 === 默认是 fail-open 的。它根本不需要去推理未设置的情况,因为未设置的情况不在写代码时所依据的示例里。
人类也会这样——fail-open 的默认行为、用 === 比较 secret、"配置显然总是会设的"。Agents 只是做得更快、ship 得更心安理得。
破绽不在代码里。在你没写的测试里。发一个没有凭证且 env var 未设置的请求,然后断言它被拒绝:
delete process.env.INTERNAL_API_TOKEN;
const res = await request(app).post('/checkout').send({ seats: 5 });
expect(res.status).not.toBe(200); // must be 401/302 — not "welcome in"
如果一个未认证请求在没配 token 的情况下拿到 200,你的 auth 就是开放的——happy path 再多绿也不会告诉你。bug 完全存在于测试从未覆盖的那个分支里。
Auth 默认拒绝。每一条授予访问权限的分支都从"否"开始。
校验两边——类型是 string,长度 > 0。undefined === undefined 不是认证。
任何 secret 用常量时间比较(crypto.timingSafeEqual),绝不用 ===。
写上那个配置错误的 env 测试——unset token,不发任何东西,断言被拒绝。
代码中读取的每一个 secret 都要放进 .env.example。缺失的一个不是配置缺口;在本案中它就是整把锁。
我是这样发现它的:在我真正的代码上复现——一个匿名请求返回了 200——应用修复,确认它变成了 302,用的是 FetchSandbox。bug 从来不在 happy path 里。它在代码从未需要处理的那些分支里。
这就是每条认证检查都应该从之出发的规则:fail closed。