FixVibe

// docs / security guides / hardening

如何安全地交付 AI 编程工具构建的应用

针对使用 Cursor、Claude Code、Lovable、Bolt、v0、Replit 或 Windsurf 构建的应用程序的分步强化指南。四个阶段:了解AI-生成的应用程序为何以不同方式失败,立即运行代码库审核,在部署时进行强化,然后继续监控。观点鲜明、叙述性强,带有可以复制的真实片段。

为什么AI-生成的应用程序失败的方式不同

Vibe 编码的应用程序可以是安全的。他们需要额外的审核通过,因为故障模式是结构性的,而不是粗心的:

  • Cursor inlines hardcoded keys. 您要求Cursor“修复身份验证错误”,它会粘贴一个假定服务角色客户端的Supabase 示例。键最终位于页面组件的顶部。匿名客户端和服务客户​​端并存;两艘船。
  • Claude Code defaults to permissive CORS. 生成的 Express / Fastify 处理程序随 cors({ origin: '*' }) 一起提供,因为这是获得工作预览的最快方法。中间件永远不会获得第二次传递。
  • Lovable and v0 skip the rules file. Firestore 支持的项目生成数据模型,但很少接触 firestore.rules。测试模式规则会静默过期并锁定数据库,不会向用户发出警告。
  • Bolt skips RLS migrations. Bolt 生成一个 Supabase 模式和一个使用匿名密钥的 CRUD 表面。 ENABLE ROW LEVEL SECURITY 永远不会进入迁移。匿名用户可以读取或写入任何行。
  • Windsurf trusts unsigned IDs. 生成的GET /api/items/[id] 读取参数并查询Postgres,而不验证所有权。该模式非常常见,因此活动 active.idor-walking 探头可以在单次扫描中将其显示出来。

立即审计:grep 您的代码库以查找风险模式

在硬化任何东西之前,先找出已经损坏的部分。这些 grep 每个都需要不到一分钟的时间:

秘密和提供商密钥

bash
grep -RIn 'NEXT_PUBLIC_SUPABASE_SERVICE' src/
grep -RIn 'sk_live_\|pk_live_\|STRIPE_SECRET' src/
grep -RIn 'sk-ant-\|^sk-' src/  # Anthropic / OpenAI
grep -RIn 'AIza\|AKIA' src/        # Google / AWS
grep -RIn 'eyJh[A-Za-z0-9_-]\{20,\}' src/  # JWT-shaped strings

任何命中都需要删除加上密钥轮换。 Provider 仪表板:Supabase → 设置 → API、Stripe → 开发人员 → API 键、Anthropic / OpenAI 控制台。

数据库访问控制

bash
# Supabase migrations
grep -RIn 'CREATE TABLE public\.' supabase/migrations/
grep -RIn 'ENABLE ROW LEVEL SECURITY\|FORCE ROW LEVEL SECURITY' supabase/migrations/

# Firebase / Firestore
cat firestore.rules  # confirm no `if true;` matches

每个CREATE TABLE public.* 都需要一个匹配的ENABLE ROW LEVEL SECURITY 和至少一个策略。 Firestore 规则的读取范围必须为 request.auth.uid

身份验证和会话处理

bash
grep -RIn 'getSession()' src/   # should be getUser() server-side
grep -RIn 'localStorage\.\(set\|get\)Item.*token' src/
grep -RIn 'jwt.verify.*\(noVerify\|skipVerify\)' src/

服务器渲染的路由必须使用supabase.auth.getUser() - 它与后端进行验证。 getSession() 读取未经验证的 cookie。 localStorage 中的令牌可供页面上运行的任何脚本访问。

标头和中间件

bash
# Confirm middleware location for src/ layouts
ls src/middleware.ts middleware.ts 2>&1

# Look for CSP and security headers
grep -RIn 'Content-Security-Policy\|Strict-Transport-Security' src/

对于 src/ 布局,仅选取 src/middleware.ts。如果您的中间件文件位于项目根目录,Next.js 会默默地忽略它,并且您的 CSP / auth-refresh 逻辑永远不会运行。

部署时强化

一旦源代码干净,就锁定应用程序如何进入生产环境。

第 1 步:单独的环境

Vercel:三个环境 - Production(您的产品域)、预览(PR/分期部署)、开发(本地)。每个都有自己的环境变量集。 Live Stripe / Anthropic / Supabase 键永远不会到达预览;预览键永远不会达到Production。分支自动推送到预览;合并到main部署到Production。

第2步:通过中间件严格CSP

生成每个请求的随机数,然后将其注入Content-Security-Policy。当您设置 x-nonce 请求标头时,Next.js 自动将随机数应用于其自己的脚本标记。

ts
// src/middleware.ts
import { NextResponse, type NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const nonce = crypto.randomUUID().replace(/-/g, '');
  const csp = [
    `script-src 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'self' 'unsafe-inline'`,
    `img-src 'self' data: https:`,
    `connect-src 'self' https://*.supabase.co`,
    `object-src 'none'`,
    `base-uri 'self'`,
    `frame-ancestors 'none'`,
  ].join('; ');

  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-nonce', nonce);

  const response = NextResponse.next({ request: { headers: requestHeaders } });
  response.headers.set('Content-Security-Policy', csp);
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  return response;
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

步骤 3:在每个公共表上强制RLS

RLS 默认情况下不启用,并且不会对表所有者强制执行,除非您 FORCE 它。将每个表与每个角色的明确策略配对。

sql
-- supabase/migrations/XXXX_rls.sql
alter table public.profiles enable row level security;
alter table public.profiles force row level security;

create policy "profiles: read own"
  on public.profiles for select
  using (auth.uid() = id);

create policy "profiles: update own"
  on public.profiles for update
  using (auth.uid() = id)
  with check (auth.uid() = id);

第 4 步:对每个 API 路由进行仅服务器身份验证

每个状态更改API 路由都会使用supabase.auth.getUser() 验证调用者服务器端。用户对象成为user_id 的真实来源——永远不要相信请求正文来设置它。

ts
// src/app/api/items/route.ts
import { NextResponse, type NextRequest } from 'next/server';
import { createClient } from '@/lib/supabase/server';

export async function POST(request: NextRequest) {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) return NextResponse.json({ error: 'unauthorized' }, { status: 401 });

  const body = await request.json();
  const { data, error } = await supabase
    .from('items')
    .insert({ ...body, user_id: user.id })  // server-supplied, not from body
    .select()
    .single();

  if (error) return NextResponse.json({ error: error.message }, { status: 400 });
  return NextResponse.json(data);
}

第 5 步:反向代理您的分析

通过您自己的域进行Proxying 分析可以避免广告拦截器,并让您的CSP connect-src 'self' 保持狭窄。相同的模式适用于 PostHog、Plausible、Umami、自定义事件接收器。

ts
// src/app/api/posthog/[...path]/route.ts
import { type NextRequest } from 'next/server';

const UPSTREAM = 'https://us.i.posthog.com';

export async function POST(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
  const { path } = await params;
  const url = `${UPSTREAM}/${path.join('/')}`;
  return fetch(url, {
    method: 'POST',
    headers: { 'content-type': req.headers.get('content-type') ?? 'application/json' },
    body: await req.text(),
  });
}

第 6 步:在身份验证后反弹时打开重定向防护

登录/注册流程通常接受 next 查询参数。拒绝任何不是同站点路径的内容 - 以 / 开头,而不是 // (相对于协议,将用户发送到场外)。

ts
function safeNext(raw: string | null): string {
  if (!raw) return '/dashboard';
  if (!raw.startsWith('/') || raw.startsWith('//')) return '/dashboard';
  return raw;
}

正在进行:监控和重新扫描

每次部署都会发生漂移。将安全视为一个循环,而不是您完成的清单。

验证您的生产域

Dashboard → Domains → 添加您的产品域 → DNS TXT 或 HTTP- 文件验证(单步)。一旦经过验证,主动扫描就变得可用,并且可以启用计划的重新扫描。

安排被动重新扫描

Hobby 每天一次,Pro 每 3 小时一次,Unlimited 每小时一次。如果出现新的结果,每次运行都会向您发送电子邮件,如果您已订阅,则会触发 scan.completed webhook。

bash
# Or from CI, via the REST API:
curl -X POST https://fixvibe.app/api/v1/scans \
  -H "authorization: Bearer $FIXVIBE_TOKEN" \
  -H "content-type: application/json" \
  -d '{"target":"https://your-app.com"}'

启用API-主动扫描(可选)

如果您想要自动主动探测(SQLi / XSS / IDOR 步行 / 等),请在仪表板 → 域 → API 活动中为每个域打开它。授权是持久的,90 天到期,可立即撤销。与 scan.active_api.first_used webhook 配对,以便启用后的第一次自动主动扫描到达您的警报。

将调查结果连接到您的AI 工作流程中

Mint an API token at Account → API tokens, then configure the MCP server (/docs/mcp) in Claude Desktop / Cursor / Continue. Ask your agent: "Run a scan on staging and show me the highest-severity findings." The agent calls FixVibe, fetches the report, and renders categorized remediation guidance so code/config fixes become prompts and DNS/provider/manual fixes become operator steps.

实时威胁检测 (Unlimited)

证书透明度日志差异显示为您的域颁发的新 TLS 证书。 DNS-记录差异捕获未经授权的更改。 JS-bundle 秘密监控在新密钥到达已发货的捆绑包时就会触发。威胁情报源(Spamhaus、URLhaus)会报告您的域(如果已列出)。

真实的故障模式及其修复

来自生产环境的五种模式扫描了数千个AI-生成的应用程序,每种模式都有实际的修复:

  1. 客户端组件中的服务角色键

    Symptom: baas.supabase-service-key 在生产中发现URL。 Cause: Cursor 自动完成将 createClient(URL, SERVICE_ROLE_KEY) 粘贴到 React 组件中。 Fix: 将服务客户端移动到 src/lib/supabase/service.tsimport 'server-only' 位于顶部;使用匿名密钥创建并行src/lib/supabase/client.ts供客户端使用;通过 Supabase Studio 轮换服务角色密钥。

  2. Firestore 规则仍处于测试模式

    Symptom: baas.firebase-rules 高严重性发现。 Cause: 生成的规则为 allow read, write: if request.time < timestamp.date(2026, 6, 1); — 有时间限制的“允许所有”。 Fix: 将每个规则的范围限定为经过身份验证的用户 — match /users/{userId}/posts/{postId} { allow read, write: if request.auth.uid == userId; } — 并重新部署 firebase deploy --only firestore:rules

  3. 允许CORS 生存到生产环境

    Symptom: active.cors 高严重性。 Cause:生成的Express中间件:app.use(cors({ origin: '*' }))Fix: 将您的前端来源列入白名单:app.use(cors({ origin: ['https://your-app.com'], credentials: true }))。对于 Next.js API 路由,请在响应中显式设置Access-Control-Allow-Origin

  4. RLS 启用但不强制

    Symptom: active baas.supabase-rls 报告匿名角色可以写入公共表,即使在仪表板中启用了RLS。 Cause: ENABLE 没有 FORCE 会使表所有者豁免 - 并且迁移作为所有者运行。 Fix:alter table public.items force row level security; 附加到迁移中。重新部署。

  5. 未签名IDOR-walkable ID

    Symptom: active.idor-walking 报告匿名用户可以跨租户读取/api/items/1/api/items/2、...。 Cause: API 处理程序信任路径参数并在没有所有权谓词的情况下查询 Postgres。 Fix: 在每个读取查询上添加.eq('user_id', user.id),或移至/api/users/[uid]/items/[id] 范围内的签名 URL/UUID。

振动代码安全循环

我们的目标不是完美的安全性,而是完美的安全性。它消除了 AI 工具经常错过的容易实现的目标,以便您可以保持快速交付。

  1. Generate fast — 使用Cursor、Claude Code、Lovable、Bolt。这就是重点。
  2. Audit immediately — 运行上面设置的 grep,检查RLS,验证CSP,检查身份验证边界。
  3. Harden at deploy — 中间件、环境分离、CSP 随机数、HSTS、仅服务器身份验证。
  4. Monitor — FixVibe 每天被动,每周在经过验证的域上活跃,Slack 的 Webhook,Unlimited 上的威胁检测。
  5. Fix fast — use FixVibe coding-agent prompts for code/config findings and operator steps for DNS, provider, secret-rotation, or manual-review findings. Re-deploy, re-scan, close the loop.

后续步骤

有关DAST 与SAST 的概念背景以及为什么AI- 生成的应用程序需要自己扫描,请阅读AI-generated code security scanning。有关发货前审核的快速参考,请参阅vibe coding security checklist

// scan your app

别再读了,去找你应用里的漏洞。

插入 URL — FixVibe 在不到一分钟的时间内运行本指南中的每项被动检查以及 200 多项其他检查。 Free,无需安装,无卡。

  • Free 层 — 每月 3 次扫描,无卡。
  • 针对任何 URL 进行被动扫描 — 无需域验证。
  • 针对 Cursor、Claude Code、Lovable、Bolt、v0、Replit 进行了调整。
  • Coding-agent prompts for code/config findings, plus operator steps for DNS/provider fixes.
如何安全地交付 AI 编程工具构建的应用 — Docs · FixVibe