你让 Cursor 在 /api/admin/export-users 加个接口。它生成 handler,挂在 Express app 的最顶层,返回 200 加全量用户列表,在 dev 里跑得完美——因为 dev 根本没有 auth。到了 staging,有人不登录就敲这个 URL,拿到所有用户的邮箱。这条路由被注册在你 requireAuth 中间件边界之外。AI 没看到边界,它只看到”加一条路由”,然后加在它眼里”路由通常在的地方”。这是 P0 级安全事故,而且会发生,是因为鉴权边界通常是结构性的(某个 router 用了中间件 X,另一个没用),而不是 AI 可以模式匹配的逐路由声明。
常见原因
按真实审计中的频次排序。
1. AI 把路由注册在了根 app 上,而不是鉴权 router 上
你的 Express 结构是:
app.use("/api/public", publicRouter);
app.use("/api", requireAuth, authedRouter);
app.use(adminRouter); // <- AI 把新路由加在这里
新的 admin 路由挂在裸 app 上,requireAuth 永远不会执行。
如何识别:新路由文件 import 了 app(或根 router),并直接 app.get(...),而不是用既有的鉴权 router。
2. AI 把路由文件放到了”受保护目录约定”之外
你的项目约定是:src/routes/authed/ 下的一切自动套 requireAuth;AI 在它隔壁建了 src/routes/export-users.ts,在那个目录外。auto-loader 收不到,AI 又看到类似 import 就手动挂——通常挂在顶层 app。
如何识别:新路由文件在不寻常的目录;它的 import 路径与同级路由不一致。
3. AI 删了某个中间件,以”修”dev 里的 401
接口在本地一直 401。AI 的诊断是”把挡测试请求的中间件去掉”,而不是”测试请求要登录”。结果这条路由在所有环境都不需要 auth。
如何识别:diff 里删除了 requireAuth 的 import,或路由注册里 app.use(requireAuth, ...) 那行没了。
4. AI 用 if (req.user?.isAdmin) 守了,但 auth 加载步骤被跳过
接口里检查 req.user.isAdmin,但 req.user 根本没被填——因为从 JWT 设置 req.user 的上游中间件就没被接上。没有 auth 的情况下 req.user 是 undefined,在乐观分支里 fail-close 没问题,但 AI 在另一个分支里又返回了默认值。
如何识别:代码引用了 req.user,但在该路由中间件链上游找不到任何 req.user = ...。
5. 在 Next.js / Astro / Remix 里没用对路由组
框架约定里 app/(authed)/admin/users/page.tsx 由 layout 强制鉴权,app/admin/users/page.tsx 不被强制。AI 建了第二种。
如何识别:新文件在 (authed)(或等价分组)之外;对应 layout.tsx 没有 auth。
6. AI 加的 OPTIONS / CORS 预检处理把数据顺带返回了
某些 AI 建议会用对 OPTIONS 提前返回来处理 CORS。如果提前返回里塞了数据(罕见但确实有),响应就先于任何 auth 检查泄漏出去。
如何识别:OPTIONS handler 返回 200 且带 body;auth 中间件只对 GET/POST 跑。
7. AI 加的 webhook 或内部接口,共享密钥校验写错了
webhook handler 经常用 secret header 代替 session 鉴权。AI 实现里常见:用 == 而非 crypto.timingSafeEqual 比较 secret;把空 secret 当合法;header 缺失时直接放过(if (header) { check } else { allow })。
如何识别:新路由用了 secret header;比较朴素,或有”header 缺失则放行”的兜底。
开始之前
- 盘点应用所有公开路由:
grep -r "app\.\(get\|post\|put\|delete\|patch\)" src/以及你框架的等价命令。 - 把每条路由对应到它的鉴权中间件。链上无中间件的路由都是嫌疑对象。
- 如果已经泄漏:按安全事故处理——轮换泄漏的密钥、审计访问日志、按 IR 流程上报。
需要收集的信息
- AI 新加路由 handler 的文件路径。
- 路由注册代码——它是如何被挂进 app 的。
- 项目里”鉴权 vs 公开”路由的标准模式。
- 本应套上的中间件链(
requireAuth、requireAdmin等)。 - 最近加路由的 commit 都要逐个看。
- 这个无防护接口自上线以来的访问日志。
分步修复
按”先堵漏,再防再犯”排序。
第 1 步:如果在泄漏数据,立刻禁用这条路由
把注册注释掉,或在 handler 里返回 503,边调查边阻断:
// app.get("/api/admin/export-users", exportUsersHandler); // DISABLED 2026-05-24 — missing auth
或加一个临时 feature flag,默认关。
第 2 步:把路由迁回鉴权 router 内
重构:
// 错:
app.get("/api/admin/export-users", exportUsersHandler);
// 对:
adminRouter.get("/export-users", exportUsersHandler);
app.use("/api/admin", requireAuth, requireAdmin, adminRouter);
中间件链通过挂载点生效。handler 保持纯粹。
第 3 步:在 app 根上加 deny-by-default 中间件
Express:
app.use((req, res, next) => {
if (!req.user && !req.path.startsWith("/api/public")) {
return res.status(401).json({ error: "auth required" });
}
next();
});
Next.js 的 middleware.ts:
export function middleware(req: NextRequest) {
if (req.nextUrl.pathname.startsWith("/api/public")) return NextResponse.next();
const token = req.cookies.get("session");
if (!token) return new NextResponse("auth required", { status: 401 });
return NextResponse.next();
}
这样哪怕 AI 把路由挂在了保护组之外,也会撞上 deny 默认。
第 4 步:把 req.user.isAdmin 检查换成有类型的 helper
// src/lib/auth.ts
export function requireAdmin(req: Request, res: Response, next: NextFunction) {
if (!req.user) return res.status(401).json({ error: "auth required" });
if (!req.user.isAdmin) return res.status(403).json({ error: "admin required" });
next();
}
路由文件里:
adminRouter.get("/export-users", requireAdmin, exportUsersHandler);
handler 自身不写鉴权逻辑,AI 也就更难悄悄改坏。
第 5 步:为每条受保护路由加一条安全测试
describe("admin endpoints require auth", () => {
const adminRoutes = ["/api/admin/export-users", "/api/admin/users", "/api/admin/billing"];
for (const route of adminRoutes) {
test(`${route} returns 401 without auth`, async () => {
const res = await request(app).get(route);
expect(res.status).toBe(401);
});
}
});
AI 加新 admin 路由时,把它加进列表。一旦路由绕开了 auth,这个测试立刻挂。
第 6 步:对路由文件做静态检查
简单的 grep 或自定义 AST 检查:
# CI 脚本里
git diff --name-only main | grep "src/routes/" | while read f; do
if ! grep -q "requireAuth\|publicRoute" "$f"; then
echo "ERROR: $f does not declare auth posture"
exit 1
fi
done
强制每个路由文件显式声明 auth 立场。什么都不说的文件 PR 阶段就被拦下。
第 7 步:用规则文件教 AI
.cursorrules / CLAUDE.md:
## Auth posture (required for any new route)
- All routes default to authenticated. Never register a route on the root app
unless it is for /api/public/* or /api/webhooks/* (which use signature verification).
- Use the existing `adminRouter`, `authedRouter`, or `publicRouter` — do NOT
create a new top-level router.
- Auth checks live in middleware, never in handler bodies.
- When adding a new admin route: file goes in src/routes/admin/, mounted under
adminRouter, which is mounted with requireAuth + requireAdmin.
- Webhook endpoints use src/lib/webhook-verify.ts — never roll your own
signature comparison.
Before adding a route, restate the auth posture in your plan.
验证
- 不带 auth 命中新接口,本地和 staging 都返回 401。
- auth 测试套件包含了新路由,且全部通过。
grep显示 handler 只挂在鉴权 router 下,根 app 上没有。- 修复前的访问日志已审过;任何未授权访问都按 IR 流程跟进/上报。
- 静态检查 / lint 规则能拦下未来不声明 auth 立场的路由。
长期预防
- 在框架层做 deny-by-default,这样 AI 漏挂的路由也无法泄漏。
- 每个 auth tier 只有一条权威 router(public、authed、admin)。新路由挂到既有 router,而不是新建。
- auth 放在中间件,绝不写成 handler 内的条件。AI 经常重写 handler 体,但很少重构中间件链。
- 新路由文件必须有一行
// auth: <public|authed|admin>注释,lint 强制。 - 每季度跑一次路由审计:
grep -r "router\.\(get\|post\)" src/,确认每条都映射到已知 auth tier。 - AI 生成的路由改动必须在同一 PR 里附带安全测试,没测试不合并。
常见误区
- 相信 AI 那句”我加了鉴权”,但没检查路由注册位置——它可能把检查写在了 handler 里,位置错了。
- 默许 AI 自创顶层
app.use(...),而不是用既有鉴权 router。 - 把 dev 那种宽松鉴权(“dev 里人人是 admin”)当成 AI 可以依赖的事实。测试得对接近 staging 的 auth。
- 只 review handler 的 diff,跳过路由注册的 diff。bug 几乎都在注册,不在 handler。
- 允许 AI 在单文件里临时定义
requireAuth。auth helper 应在src/lib/auth.ts(或等价位置)并被处处 import。 - 忘了同时保护 HEAD、OPTIONS 等少被测试的方法。
相关问题见 AI 删掉了能跑的逻辑、AI 测试通过但功能其实坏了、AI 回滚改动。
FAQ
Q:AI 的 handler 里检查了 req.user.isAdmin,够了吗?
不够。如果填 req.user 的鉴权中间件根本没跑,req.user 是 undefined,你的检查就取决于 undefined 分支怎么处理。auth 必须在 handler 之前跑——放中间件,不要放 handler 体。
Q:我们用 session cookie,浏览器自动带,AI 加的路由应该没问题吧?
只有当读取 cookie、验证 session 的中间件对该路由跑了才行。路由挂在哪里决定哪些中间件跑。AI 在这点上静默出错。
Q:怎么审计 AI 历史 commit 里的同类 bug?
git log --diff-filter=A --name-only src/routes/ 列出所有新加路由文件。每一个都核对注册位置在鉴权 router 之下。30 分钟就能筛出最严重的。
Q:要不要直接禁止 AI 碰 auth 代码?
太死板——AI 写 handler 体和测试是好帮手。可以限制它修改中间件链和路由注册文件,要求显式 review。把这些文件加进 .cursorignore,或要求 maintainer review label。
标签: #排查 #AI 编程 #安全 #auth #middleware