[控场AI]
· 11 分钟阅读· 5,982 字

AI代理常犯的7个安全漏洞:Next.js+Supabase+Stripe

AI代理常犯的7个安全漏洞:Next.js+Supabase+Stripe

AI代理在Next.js+Supabase+Stripe应用中常走捷径,制造7类可快速点击验证的安全漏洞。

文章列举了AI编程代理在构建Next.js + Supabase + Stripe应用时最常引入的7类安全漏洞,包括:RLS策略写成 `using (true)` 形同虚设、用service-role密钥绕过RLS、服务器端误信 `getSession()`、Stripe webhook先解析JSON导致签名失效、webhook非幂等、从URL成功重定向授予访问、认证回调开放重定向,以及缺失环境变量时静默失败。每个漏洞都配有可复制的修复代码片段,并给出明确规则,例如服务器授权必须用 `getUser()`、webhook必须读取 `request.text()` 并验证签名、付费访问只能由webhook写入的数据库状态决定。作者建议将这些规则写入AGENTS.md、CLAUDE.md或.cursor/rules,让代理在编码前就避免大多数问题。

AI编程代理擅长让Next.js + Supabase + Stripe应用跑起来,但远不擅长让它安全。以下漏洞都能通过快速点击测试,这正是它们能上线的原因。

AI代理安全漏洞示意图

1. RLS已启用,但策略形同虚设

代理常写 create policy "allow all" on public.subscriptions for select using (true); 来消除空查询。正确做法是按登录用户限定所有权,且仅服务器可写的表不应有写策略。

alter table public.subscriptions enable row level security;
create policy "read own subscriptions" on public.subscriptions
  for select to authenticated using ((select auth.uid()) = user_id);

将 auth.uid() 包裹在 (select ...) 中可让Postgres每查询求值一次而非每行一次。查找现有问题:

select tablename, policyname from pg_policies where qual = 'true';

规则: public下每个表都启用RLS。绝不写 using (true)。若查询无返回,修策略,绝不换密钥。

什么是RLS:Row Level Security(行级安全)是PostgreSQL原生特性,允许在表级别定义策略,控制每一行数据对哪个角色、哪种操作可见或可写。在Supabase中,前端使用匿名密钥(anon key)直连数据库,因此RLS是唯一的数据隔离屏障。没有RLS或策略写成 using (true),任何持有anon key的人都能读取整张表——包括他人订阅、订单、私信等。

认证角色:Supabase内置 anon(未登录)、authenticated(已登录)两个数据库角色,auth.uid() 返回当前JWT中的用户id。策略中 to authenticated 表示仅对已登录用户生效。

2. 用service-role密钥“修复”空结果

当RLS正确返回空时,代理常换用secret/service-role密钥,完全绕过RLS。保持两个命名清晰的客户端,让危险者显而易见:

/** 每请求客户端,以登录用户身份行事。RLS生效。 */
export async function createUserClient() { /* ... */ }

/** 绕过RLS的管理客户端。仅用于可信服务器代码如webhook。 */
export function createAdminClient() { /* ... */ }

在该文件顶部加 import "server-only";,若客户端组件导入则构建失败。

规则: 管理客户端仅用于webhook处理器和后台任务,绝不用于代表用户的代码。

service-role密钥与anon密钥的区别:Supabase项目提供两类API密钥。anon密钥是公开的,受RLS约束,只能以登录用户身份行事;service-role密钥拥有数据库超级用户权限,完全绕过RLS,绝不可出现在浏览器或客户端组件中。一旦将service-role密钥误打包前端,攻击者即可读写整个数据库。

server-only 包:这是Next.js官方提供的npm包,导入后若被客户端组件('use client')引入会直接构建失败,用于在编译期阻止敏感服务器代码泄露到浏览器。

3. 服务器端信任getSession()

supabase.auth.getSession() 读取cookie而不向Supabase Auth重新验证。服务器上做重要操作前应正确检查用户:

const supabase = await createUserClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: "Not signed in" }, { status: 401 });

若使用非对称JWT签名密钥,getClaims() 也可。

规则: 服务器端授权用 getUser()/getClaims(),绝不用 getSession()。

getSession() 与 getUser() 的本质区别:getSession() 只是从cookie中读取JWT并解码,不向Supabase Auth服务器验证签名与有效期,因此cookie可被伪造。getUser() 会携带该JWT调用Supabase Auth的 /user 接口,由服务器验证令牌真实性后返回用户信息。在服务器端进行授权判断(如判断管理员、扣费、写库)前必须使用 getUser(),否则攻击者可通过伪造cookie冒充任意用户。

getClaims():当项目启用了非对称JWT签名密钥(RS256/ES256)时,Supabase Auth的JWT可被本地公钥验证,getClaims() 能在不发起网络请求的情况下安全校验令牌,适合高频调用场景。

4. 验证前解析Stripe webhook体

await request.json() 重新序列化载荷导致签名检查失败。代理通过删除检查来“修复”。应读取原始文本:

export const runtime = "nodejs";

export async function POST(request: Request) {
  const signature = request.headers.get("stripe-signature");
  if (!signature) return NextResponse.json({ error: "Missing signature" }, { status: 400 });

  // 签名验证需要原始体。绝不先解析JSON。
  const body = await request.text();
  let event: Stripe.Event;
  try {
    event = stripe().webhooks.constructEvent(body, signature, env().STRIPE_WEBHOOK_SECRET);
  } catch {
    return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
  }
  // ...
}

同时从认证中间件/代理排除webhook路径,避免重定向到登录页。

规则: webhook处理器读取 request.text() 并在处理载荷前验证签名。

为什么必须用原始体验证签名:Stripe使用HMAC-SHA256对webhook请求的原始字节(raw body)计算签名,并用 Stripe-Signature 头传递。request.json() 会先解析再序列化,字段顺序、空白、转义可能发生变化,导致签名校验失败。许多教程或AI代理遇到失败后,会直接删除签名验证代码,从而使任何人可伪造“支付成功”事件。

运行时要求:在Next.js App Router中,必须显式设置 export const runtime = "nodejs",因为Edge Runtime默认会消费请求体;同时要确保认证中间件(middleware)不拦截 /api/webhooks/stripe 路径,避免请求被重定向到登录页。

5. 非幂等webhook

Stripe重试投递且可能多次发送事件。用主键记录每个事件id并跳过重复:

create table public.stripe_events (
  id text primary key,
  type text not null,
  received_at timestamptz not null default now()
);
alter table public.stripe_events enable row level security; -- 无策略=无客户端访问
const { error: dupError } = await db.from("stripe_events").insert({ id: event.id, type: event.type });
if (dupError?.code === "23505") return NextResponse.json({ received: true, duplicate: true });
if (dupError) return NextResponse.json({ error: "DB error" }, { status: 500 });

真实DB错误返回500也重要:告诉Stripe稍后重试而非静默丢失事件。

规则: 每个webhook对 event.id 幂等;用 upsert 处理状态。

6. 从成功重定向授予访问

/account?checkout=success 只是URL——任何人可访问。访问必须来自webhook镜像的订阅状态,映射应显式:

export function accessFor(status: string | null | undefined): "active" | "grace" | "none" {
  switch (status) {
    case "active":
    case "trialing":
      return "active";
    case "past_due":
      return "grace";
    default:
      return "none";
  }
}

在结账创建时将Stripe客户链接回用户(client_reference_id: user.id),让webhook知道订阅属于谁——绝不从请求体取用户id或价格。

规则: 付费访问仅源于webhook写入的行。价格来自env/服务器配置,绝不来自客户端。

7. 认证回调中的开放重定向

魔法链接和OAuth回调常接受 ?next= 参数,代理直接传给 redirect(),使登录变成钓鱼重定向器(?next=//evil.com)。仅允许同源相对路径:

export function safeNext(next: string | null | undefined, fallback = "/account"): string {
  if (!next || !next.startsWith("/") || next.startsWith("//") || next.startsWith("/\\")) return fallback;
  return next;
}
const next = safeNext(url.searchParams.get("next"));
return NextResponse.redirect(new URL(ok ? next : "/login?error=link-invalid", url.origin));

规则: 任何来自用户输入的重定向目标都经过 safeNext()。

附加:坏env时大声失败

缺失 STRIPE_WEBHOOK_SECRET 不应在运行时变成 undefined。用zod惰性验证(让 next build 仍可在无秘密时工作):

const schema = z.object({
  SUPABASE_SECRET_KEY: z.string().min(1),
  STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
  STRIPE_WEBHOOK_SECRET: z.string().startsWith("whsec_"),
  STRIPE_PRICE_ID: z.string().startsWith("price_"),
  // ...
});
let cached: z.infer<typeof schema> | undefined;
export function env() { return (cached ??= schema.parse(process.env)); }

总结

这些并非罕见问题。它们是代理以“让错误消失”为唯一目标时采取的必然捷径。将规则写在代理每次都会读取的地方(AGENTS.md、CLAUDE.md、.cursor/rules),大多数问题在写下之前就能避免。

披露:我制作了这些资源。 我将其转化为一份免费的30点Supabase + Stripe预发布安全检查清单。若需要完整规则文件及这些代码片段来源的参考应用,有一个15美元包(使用DEVTO25享25%折扣)。无论如何,希望上述列表能帮你避免糟糕的发布日——欢迎在评论中纠正。

#AI代理安全漏洞#Next.js安全#Supabase RLS#Stripe webhook安全#AI编程安全#Web应用安全#AGENTS.md规则#开放重定向
分享:

相关推荐