// docs / security guides / hardening
AI 코딩 도구로 만든 앱 보안 가이드
Cursor, Claude Code, Lovable, Bolt, v0, Replit 또는 Windsurf로 구축한 앱에 대한 단계별 강화 가이드입니다. 4단계: 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 스키마와 anon 키를 사용하는 CRUD 표면을 생성합니다.
ENABLE ROW LEVEL SECURITY은 마이그레이션에 참여하지 않습니다. 익명 사용자는 모든 행을 읽거나 쓸 수 있습니다. - Windsurf trusts unsigned IDs. 생성됨
GET /api/items/[id]은 소유권을 확인하지 않고 매개변수를 읽고 Postgres에 쿼리합니다. 패턴은 활성 active.idor-walking 프로브가 단일 스캔 내에서 패턴을 표면화할 만큼 충분히 일반적입니다.
즉각적인 감사: 위험 패턴에 대한 코드베이스 수집
무엇이든 굳히기 전에 이미 깨진 것이 무엇인지 찾아보세요. 이러한 grep은 각각 1분 미만이 소요됩니다.
비밀 및 공급자 키
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()은 확인되지 않은 쿠키를 읽습니다. 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 / 스테이징 배포), 개발(로컬) 각각은 자체 env-var 세트를 갖습니다. 라이브 Stripe / Anthropic / Supabase 키는 미리보기에 도달하지 않습니다. 미리보기 키는 Production에 도달하지 않습니다. 분기는 자동으로 미리보기로 푸시됩니다. main에 병합하면 Production에 배포됩니다.
2단계: 미들웨어를 통한 엄격한 CSP
요청별 nonce를 생성한 다음 Content-Security-Policy에 삽입합니다. x-nonce 요청 헤더를 설정할 때 Next.js은 자체 스크립트 태그에 nonce를 자동 적용합니다.
// 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을 사용하지 않는 한 테이블 소유자에게 적용되지 않습니다. 각 테이블을 역할별 명시적 정책과 연결합니다.
-- 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 → prod 도메인 추가 → DNS TXT 또는 HTTP- 파일 확인(단일 단계). 확인되면 활성 검사를 사용할 수 있게 되며 예약된 재검사를 활성화할 수 있습니다.
수동 재검사 예약
매일 Hobby, 3시간마다 Pro, 매시간 Unlimited. 각 실행에서 새로운 결과가 나타나면 이메일을 보내고 구독한 경우 scan.completed 웹훅을 실행합니다.
# 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 웹후크와 페어링하세요.
결과를 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-record diff는 승인되지 않은 변경 사항을 포착합니다. JS-bundle 비밀 모니터링은 새 키가 배송된 번들에 도달하는 순간 실행됩니다. 위협 정보 피드(Spamhaus, URLhaus)는 도메인이 목록에 있는 경우 이를 보고합니다.
실제 실패 패턴 및 수정 사항
AI- 생성된 수천 개의 앱에 대한 프로덕션 스캔의 5가지 패턴(각각 실제 수정 사항 포함):
- 클라이언트 구성 요소의 서비스 역할 키
Symptom:
baas.supabase-service-key제작진의 발견 URL. Cause: Cursor 자동 완성을 통해createClient(URL, SERVICE_ROLE_KEY)을 React 구성 요소에 붙여넣었습니다. Fix: 서비스 클라이언트를import 'server-only'이 맨 위에 있는src/lib/supabase/service.ts으로 이동합니다. 클라이언트측 사용을 위해 익명 키를 사용하여 병렬src/lib/supabase/client.ts을 생성합니다. Supabase Studio를 통해 서비스 역할 키를 순환합니다. - 테스트 모드에 남아 있는 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을 다시 배포합니다. - 허용적 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을 명시적으로 설정합니다. - RLS 활성화되었지만 강제되지는 않았습니다.
Symptom: 활성
baas.supabase-rls은 대시보드에서 RLS이 활성화된 경우에도 anon 역할이 공개 테이블에 쓸 수 있다고 보고합니다.FORCE이 없는 Cause:ENABLE은 테이블 소유자를 제외하고 마이그레이션은 소유자로 실행됩니다. Fix:은 마이그레이션에alter table public.items force row level security;을 추가합니다. 재배포합니다. - 서명되지 않은 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 도구가 지속적으로 놓치는 낮게 매달린 과일을 제거하여 계속 빠르게 배송할 수 있습니다.
- Generate fast — Cursor, Claude Code, Lovable, Bolt을 사용합니다. 그게 요점입니다.
- Audit immediately — 위의 grep 세트를 실행하고, RLS을 확인하고, CSP을 확인하고, 인증 경계를 검토합니다.
- Harden at deploy — 미들웨어, 환경 분리, CSP nonce, HSTS, 서버 전용 인증 확인.
- Monitor — FixVibe 매일 수동적으로, 확인된 도메인에서 매주 활성, Slack에 대한 웹후크, 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을 참조하세요.
