FixVibe

// docs / security guides / hardening

AI 코딩 도구로 만든 앱 보안 가이드

Cursor, Claude Code, Lovable, Bolt, v0, Replit 또는 Windsurf로 구축한 앱에 대한 단계별 강화 가이드입니다. 4단계: 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 SECURITY never 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은 각각 1분 미만이 소요됩니다.

비밀 및 공급자 키

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()은 확인되지 않은 쿠키를 읽습니다. 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 / 스테이징 배포), 개발(로컬) 각각은 자체 env-var 세트를 갖습니다. 라이브 Stripe / Anthropic / Supabase 키는 미리보기에 도달하지 않습니다. 미리보기 키는 Production에 도달하지 않습니다. 분기는 자동으로 미리보기로 푸시됩니다. main에 병합하면 Production에 배포됩니다.

2단계: 미들웨어를 통한 엄격한 CSP

요청별 nonce를 생성한 다음 Content-Security-Policy에 삽입합니다. x-nonce 요청 헤더를 설정할 때 Next.js은 자체 스크립트 태그에 nonce를 자동 적용합니다.

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 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.

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 → 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.

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 웹후크와 페어링하세요.

결과를 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:

  1. 클라이언트 구성 요소의 서비스 역할 키

    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 to src/lib/supabase/service.ts with import 'server-only' at the top; create a parallel src/lib/supabase/client.ts using the anon key for client-side use; rotate the service-role key via Supabase Studio.

  2. 테스트 모드에 남아 있는 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-deploy firebase deploy --only firestore:rules.

  3. 허용적 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, set Access-Control-Allow-Origin explicitly in the response.

  4. 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 the anon role through, or the migration that tightened it never ran in production. Fix: replace the permissive policy with one scoped to auth.uid(), apply the migration, and re-scan.

  5. 서명되지 않은 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 도구가 지속적으로 놓치는 낮게 매달린 과일을 제거하여 계속 빠르게 배송할 수 있습니다.

  1. Generate fast — Cursor, Claude Code, Lovable, Bolt을 사용합니다. 그게 요점입니다.
  2. Audit immediately — 위의 grep 세트를 실행하고, RLS을 확인하고, CSP을 확인하고, 인증 경계를 검토합니다.
  3. Harden at deploy — 미들웨어, 환경 분리, CSP nonce, HSTS, 서버 전용 인증 확인.
  4. Monitor — FixVibe 매일 수동적으로, 확인된 도메인에서 매주 활성, Slack에 대한 웹후크, 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

그만 읽고, 당신 앱의 취약점을 직접 찾아보세요.

Drop in a URL — FixVibe runs every passive check from this guide plus the rest of its 230+ passive checks, usually in under a minute. Free, no install, no card.

  • 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 코딩 도구로 만든 앱 보안 가이드 · FixVibe