// docs / security guides / hardening
如何安全地交付 AI 编程工具构建的应用
针对使用 Cursor、Claude Code、Lovable、Bolt、v0、Replit 或 Windsurf 构建的应用程序的分步强化指南。四个阶段:了解AI-生成的应用程序为何以不同方式失败,立即运行代码库审核,在部署时进行强化,然后继续监控。观点鲜明、叙述性强,带有可以复制的真实片段。
为什么AI-生成的应用程序失败的方式不同
Vibe 编码的应用程序可以是安全的。他们需要额外的审核通过,因为故障模式是结构性的,而不是粗心的:
- Assistants inline hardcoded keys. You ask for a fix to an auth error and get a pasted Supabase example that assumes a service-role client. The key ends up at the top of a page component. Both the anon client and the service client coexist; both ship.
- Generated servers default to permissive CORS. Generated Express / Fastify handlers often ship with
cors({ origin: '*' })because that's the fastest way to get a working preview. The middleware never gets a second pass. - Rules files get skipped. Firestore-backed projects generate the data model but rarely touch
firestore.rules. Test-mode rules let anyone read and overwrite data until someone replaces them. - RLS never enters the migration. A generated Supabase schema and CRUD surface use the anon key, but
ENABLE ROW LEVEL SECURITYnever enters the migration. Anonymous users can read or write any row. - Handlers trust IDs. A generated
GET /api/items/[id]reads the param and queries Postgres without verifying ownership. Active scans on a verified domain test for this (IDOR / BOLA).
立即审计:grep 您的代码库以查找风险模式
在硬化任何东西之前,先找出已经损坏的部分。这些 grep 每个都需要不到一分钟的时间:
秘密和提供商密钥
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 控制台。
数据库访问控制
# 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。
身份验证和会话处理
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 中的令牌可供页面上运行的任何脚本访问。
标头和中间件
# 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 自动将随机数应用于其自己的脚本标记。
// 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 isn't enabled by default on tables you create in SQL or migrations. Enable it on every exposed table and pair each one with explicit policies per role — that is what stops the anon and authenticated roles. FORCE only makes the table owner obey RLS too.
-- 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 的真实来源——永远不要相信请求正文来设置它。
// 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、自定义事件接收器。
// 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 查询参数。拒绝任何不是同站点路径的内容 - 以 / 开头,而不是 // (相对于协议,将用户发送到场外)。
function safeNext(raw: string | null): string {
if (!raw) return '/dashboard';
if (!raw.startsWith('/') || raw.startsWith('//')) return '/dashboard';
return raw;
}正在进行:监控和重新扫描
每次部署都会发生漂移。将安全视为一个循环,而不是您完成的清单。
验证您的生产域
Dashboard → Domains → add your production domain → DNS TXT or HTTP-file verification. Active scans require Hobby or above; scheduled re-scans require Pro or Unlimited.
安排被动重新扫描
Scheduled re-scans are available on Pro and Unlimited for verified domains. Free and Hobby scans are manual. Scheduled scans use your plan allowance. Configure completion email preferences and a scan.completed webhook if needed.
# 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 工作流程中
On Hobby or above, create an API token at Account → API tokens and configure the MCP server (/docs/mcp) in your coding tool. Ask your agent to run an authorized scan and inspect the highest-severity findings. Code fixes can use remediation prompts; provider and DNS fixes may need manual operator steps.
实时威胁检测 (Unlimited)
Periodic certificate-transparency, DNS, JS-bundle, and threat-intelligence checks report observed changes on supported signals. Alerts depend on successful polling and source availability; they do not establish continuous or complete security coverage.
真实的故障模式及其修复
Five common patterns in AI-generated apps, each with the actual fix:
- 客户端组件中的服务角色键
Symptom: FixVibe reports an exposed Supabase service-role key on the production URL. Cause: an autocomplete pasted
createClient(URL, SERVICE_ROLE_KEY)into a React component. Fix: move the service client tosrc/lib/supabase/service.tswithimport 'server-only'at the top; create a parallelsrc/lib/supabase/client.tsusing the anon key for client-side use; rotate the service-role key via Supabase Studio. - Firestore 规则仍处于测试模式
Symptom: a high-severity open Firebase rules finding. Cause: generated rules read
allow read, write: if request.time < timestamp.date(2026, 6, 1);— a time-bounded "allow all". Fix: scope each rule to the authenticated user —match /users/{userId}/posts/{postId} { allow read, write: if request.auth.uid == userId; }— and re-deployfirebase deploy --only firestore:rules. - 允许CORS 生存到生产环境
Symptom: a high-severity CORS misconfiguration finding. Cause: generated Express middleware:
app.use(cors({ origin: '*' })). Fix: allowlist your frontend origin:app.use(cors({ origin: ['https://your-app.com'], credentials: true })). For Next.js API routes, setAccess-Control-Allow-Originexplicitly in the response. - RLS 启用但不强制
Symptom: FixVibe reports that anonymous visitors can read a public table even though RLS looks enabled in the dashboard. Cause: RLS is on, but a policy such as
USING (true)lets theanonrole through, or the migration that tightened it never ran in production. Fix: replace the permissive policy with one scoped toauth.uid(), apply the migration, and re-scan. - 未签名IDOR-walkable ID
Symptom: an active scan on your verified domain reports that one user can read another user's records at
/api/items/1,/api/items/2, ... Cause: the API handler trusts the path param and queries Postgres without an ownership predicate. Fix: add.eq('user_id', user.id)on every read query, or move to signed URLs / UUIDs scoped under/api/users/[uid]/items/[id].
振动代码安全循环
我们的目标不是完美的安全性,而是完美的安全性。它消除了 AI 工具经常错过的容易实现的目标,以便您可以保持快速交付。
- Generate fast — 使用Cursor、Claude Code、Lovable、Bolt。这就是重点。
- Audit immediately — 运行上面设置的 grep,检查RLS,验证CSP,检查身份验证边界。
- Harden at deploy — 中间件、环境分离、CSP 随机数、HSTS、仅服务器身份验证。
- Monitor — FixVibe 每天被动,每周在经过验证的域上活跃,Slack 的 Webhook,Unlimited 上的威胁检测。
- 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。
