AI가 생성한 코드는 작동합니다. 실행됩니다. 올바르게 보입니다. 그리고 거의 모든 프로젝트에서 동일한 5가지 보안 취약점을 안고 출시됩니다. 이들은 이론적 위험이 아닙니다 — Claude, Cursor, Replit, Copilot에서 보안을 명시적으로 요청하지 않으면 거의 모든 코드베이스에서 나타나는 구체적인 허점입니다.
지난 1년간 수십 개의 바이브 코딩 프로젝트를 감사했으며, 동일한 5가지 실수가 최소 80%에서 나타납니다. 각각 30분 이내에 수정할 수 있습니다. 여기서 살펴봐야 할 점과 정확한 수정 방법을 소개합니다.
실수 1: 프론트엔드 코드에 하드코딩된 API 키
발생 원인: Claude에게 "Supabase에 연결해줄래" 또는 "Stripe 결제를 추가해줄래"라고 말하면, API 키를 React 컴포넌트에 직접 붙여넣은 코드를 생성합니다. 코드는 완벽하게 작동하고 — 브라우저 DevTools를 열어서 Network 탭이나 JavaScript 소스를 확인하면 누구나 시크릿 키를 볼 수 있습니다.
위험한 이유: Supabase 서비스 역할 키는 모든 데이터베이스 보안을 우회합니다. Stripe 시크릿 키로 누군가 카드에 청구하거나 환불을 발급할 수 있습니다. OpenAI 키로 누군가 귀사 계정에서 수천 달러의 API 호출을 할 수 있습니다. 봇들은 공개 GitHub 리포지토리와 배포된 사이트에서 노출된 키를 적극적으로 스캔합니다.
해결 방법: 모든 시크릿을 환경 변수로 이동합니다. Next.js에서는 NEXT_PUBLIC_으로 시작하는 변수만 브라우저에 도달합니다. 쓰기 접근, 관리자 접근 또는 금융 접근을 부여하는 모든 키는 서버 전용이어야 합니다.
키를 .env.local로 이동한 후 전체 코드베이스에서 이전 키 값을 검색하여 제거되었는지 확인합니다. 그 다음 Git 히스토리를 확인하세요 — 키가 한 번이라도 커밋되었다면, 삭제해도 리포지토리 히스토리에 남아있습니다. 노출된 적이 있는 모든 키를 재생성(로테이션)합니다. 아무리 짧은 시간이라도 노출되었다면 마찬가지입니다.
수정 시간: 15~30분.
실수 2: 행 수준 보안이 없는 Supabase 테이블
발생 원인: AI가 Supabase 테이블 생성 쿼리와 CRUD 작업을 생성하지만, 행 수준 보안(RLS)을 활성화하지 않습니다. 기본적으로 RLS가 비활성화된 Supabase 테이블은 프로젝트 URL과 anon 키를 가진 모든 사람이 완전히 접근할 수 있습니다 — 둘 다 공개되어야 합니다.
위험한 이유: RLS가 없으면 사용자 A가 사용자 B의 데이터를 조회할 수 있습니다. 브라우저 콘솔에서 간단한 fetch 호출로 모든 테이블의 모든 행을 가져올 수 있습니다. 전체 데이터베이스가 사실상 공개됩니다.
발견 방법: Supabase 대시보드 → 테이블 편집기로 이동합니다. "RLS 비활성화"로 표시된 테이블이 있으면 이 문제가 있습니다. 클라이언트 측 코드가 anon 키 대신 service_role 키를 사용하는지도 확인하세요 — 이것이 더 나쁩니다.
해결 방법: 모든 테이블에서 RLS를 활성화합니다. 그 다음 앱의 접근 패턴과 일치하는 정책을 만듭니다. 가장 일반적인 정책: 사용자는 auth.uid() = user_id인 행에 대해서만 SELECT, INSERT, UPDATE, DELETE할 수 있습니다. 한 사용자로 로그인하여 Supabase REST API를 통해 다른 사용자의 데이터에 접근을 시도하여 테스트합니다.
수정 시간: 테이블 수에 따라 30~60분.
이 내용이 도움이 되셨나요? 우리는 AI 도구, 워크플로우, 실용적인 보안 가이드에 대해 주당 하나의 심화 자료를 발행합니다. 먼저 받아보는 구독자들과 함께하세요 →
실수 3: 폼 또는 API 경로에 입력 검증 없음
발생 원인: AI가 어떤 입력이든 수락하는 폼과 들어오는 데이터를 신뢰하는 API 경로를 생성합니다. 폼에는 HTML의 "required" 속성이 있지만 서버 측 검증이 없습니다. 누군가 폼을 완전히 우회하고 원하는 페이로드를 사용하여 직접 API 요청을 보냅니다.
위험한 이유: 서버 측 검증이 없으면 공격자가 데이터베이스에 스크립트를 주입하거나(저장된 XSS), 서버를 충돌시키는 초대형 페이로드를 보내거나, 앱의 로직을 깨뜨리는 데이터(음수 가격이나 SQL을 포함한 이메일 필드)를 제출할 수 있습니다.
발견 방법: 코드베이스에서 API 경로나 서버 액션을 열어봅니다. 모든 필드의 형태, 타입, 길이를 확인하지 않으면서 req.body 또는 폼 데이터를 사용하면 이 문제가 있습니다.
해결 방법: 스키마 검증 라이브러리를 사용하여 모든 엔드포인트에 서버 측 검증을 추가합니다. Zod는 TypeScript 프로젝트의 표준입니다:
import { z } from 'zod';
const taskSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().max(2000).optional(),
priority: z.enum(['low', 'medium', 'high']),
});
// API 경로에서:
const parsed = taskSchema.safeParse(req.body);
if (!parsed.success) {
return Response.json({ error: parsed.error }, { status: 400 });
}
HTML로 렌더링되는 필드(댓글, 설명, 프로필)의 경우 표시하기 전에 DOMPurify로 출력을 정제합니다.
수정 시간: 30~60분.
실수 4: API 엔드포인트에 속도 제한 없음
발생 원인: AI는 요청하지 않으면 속도 제한을 절대 추가하지 않습니다. 앱이 노출하는 모든 API 경로는 스크립트를 가진 누구나 초당 수천 번 요청할 수 있습니다.
위험한 이유: 속도 제한이 없으면 누군가 로그인 엔드포인트를 무차별 대입 공격하거나, 목록 엔드포인트를 반복적으로 요청하여 데이터베이스를 스크래핑하거나, 서버를 압도하거나(서비스 거부), OpenAI나 Stripe 같은 외부 서비스를 호출하는 엔드포인트라면 API 할당량을 소진할 수 있습니다.
발견 방법: API 경로가 최근에 단일 IP 또는 사용자가 얼마나 많은 요청을 했는지 확인하지 않으면, 속도 제한이 없습니다.
해결 방법: Vercel에서 호스팅하는 앱의 경우 가장 간단한 방법은 개발용 메모리 기반 속도 제한기와 프로덕션용 Redis 기반 속도 제한기입니다. Vercel의 Edge Middleware는 기본 속도 제한을 처리할 수 있습니다. Upstash Redis(무료 티어)와 @upstash/ratelimit은 소량의 코드로 프로덕션급 속도 제한을 제공합니다.
합리적인 시작점: 인증된 사용자는 분당 60개 요청, 미인증 사용자는 분당 20개 요청, 로그인/가입 엔드포인트는 분당 5개 요청(무차별 대입 공격 방지).
수정 시간: 20~45분.
실수 5: 보호된 페이지 및 API에 인증 확인 누락
발생 원인: AI가 아름다운 대시보드 페이지와 사용자 데이터를 반환하는 API 경로를 생성하지만, 사용자가 실제로 로그인했는지 확인하는 미들웨어를 추가하지 않습니다. 개발 중에는 항상 로그인되어 있으므로 페이지가 "작동합니다". 프로덕션에서는 누군가 로그인 없이 대시보드 URL에 직접 접근하거나, API 엔드포인트를 요청하여 데이터를 얻을 수 있습니다.
위험한 이유: 보호된 페이지에 미인증 접근이 가능하면 누구나 사용자 대시보드, 관리 패널, 또는 개인 데이터를 URL을 추측하여 볼 수 있습니다. 보호되지 않은 API 경로는 Postman이나 curl 같은 어떤 도구로든 자격증명 없이 데이터를 가져올 수 있습니다.
발견 방법: 시크릿 브라우저 창(로그인하지 않음)을 열어 대시보드, 설정 또는 관리자 URL에 직접 이동합니다. 로그인 페이지로 리다이렉트되지 않고 콘텐츠를 볼 수 있으면 이 문제가 있습니다. 그 다음 API 경로를 직접 요청해보세요 — 유효한 세션 쿠키나 인증 토큰 없이 데이터를 반환하면, 그 경로들은 보호되지 않은 것입니다.
해결 방법: 모든 보호된 경로 전에 실행되는 인증 미들웨어를 추가합니다. Next.js App Router에서 프로젝트 루트에 middleware.ts를 생성합니다:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const session = request.cookies.get('session');
if (!session) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*', '/api/protected/:path*'],
};
인증이 필요한 모든 경로를 포함하도록 matcher를 조정합니다. 구현 후 시크릿 창에서 테스트합니다.
수정 시간: 15~30분.
향후 프로젝트에서 이를 방지하는 방법
패턴은 명확합니다: AI가 기능적으로는 작동하지만 보안을 요청하지 않으면 건너뛰는 코드를 생성합니다. 해결책은 처음부터 요청하는 것입니다.
바이브 코딩 프로젝트의 초기 프롬프트 끝에 이를 추가합니다:
보안 요구사항:
- 모든 시크릿은 환경 변수에 (절대 하드코딩 금지)
- 모든 테이블에서 Supabase RLS 활성화 및 사용자별 정책
- 모든 API 경로에 서버 측 입력 검증 (Zod 사용)
- 모든 공개 엔드포인트에 속도 제한
- 모든 보호된 경로에 인증 미들웨어
- 클라이언트 측 코드에서 서비스 역할 키 미사용
이것이 모든 것을 잡지는 못하겠지만, 나중에 정리하는 대신 첫 번째 패스에서 가장 일반적인 5가지 취약점을 제거합니다.
처음부터 더 안전한 코드를 생성하는 프롬프트를 작성하는 데 더 능숙해지고 싶다면, 우리의 무료 프롬프트 최적화 도구가 지시사항을 구조화하는 데 도움이 될 수 있습니다. 그리고 완전한 보안 점검을 원한다면, 우리의 단계별 가이드를 보세요: 클라이언트에게 제공하기 전에 바이브 코딩 앱을 보호하는 방법.
이것이 우리가 매주 하는 일입니다. AI 도구, 워크플로우, 정직한 관점에 대한 하나의 심화 자료 — 과장 없이, 불필요한 내용 없이. 우리와 함께하세요 →
공개: 이 기사의 일부 링크는 제휴 링크입니다. 우리는 직접 테스트하고 정기적으로 사용하는 도구만 추천합니다. 전체 공개 정책을 참조하세요.