Vibe Coding 보안 체크리스트: 실제로 실행해야 할 12가지 점검 사항 (2026)
요약
2026년 연구 결과, AI로 제작된 'vibe-coded' 앱의 보안 취약점이 심각한 것으로 나타났습니다. 특히 Supabase 데이터베이스의 57%가 로그인 없이도 읽기 가능했으며, 이는 열린 DB 설정이나 요청자 확인 부재 등 기본적인 오류에서 기인합니다. 본 글은 실제 배포된 앱을 대상으로 하는 12가지 보안 점검 사항과 그 중요성을 강조하며 개발자들에게 경고하고 있습니다.
핵심 포인트
- AI 코드도 취약할 수 있으므로 철저한 보안 검증이 필수입니다.
- Supabase 등 DB의 열린 설정(Open DB)은 심각한 데이터 유출 경로입니다.
- 보안 점검은 소스 코드가 아닌, 실제 빌드된 앱/라이브 URL을 대상으로 해야 합니다.
- 민감한 키는 클라이언트 측에 노출되지 않도록 서버 라우트로 옮겨야 합니다.
주요 시사점
- 2026년 8월에 라이브로 배포된 vibe-coded 앱을 스캔한 결과, 테스트할 수 있었던 Supabase 데이터베이스의 57%가 로그인 없이도 읽기 가능했습니다.
- 일반적인 원인은 열린(open) 데이터베이스, 브라우저로 전송되는 키(key), 또는 요청자를 확인하지 않는 라우트입니다. 이 세 가지 모두 테스트할 수 있는 설정 오류입니다.
- 아래 12가지 점검 사항 각각은 명령어를 제공하고, 실패했을 때의 결과가 어떻게 보이는지 보여주며, 수정하는 데 얼마나 시간이 걸리는지 알려줍니다.
- 점검 항목 중 절반은 빌드된 앱 또는 라이브 URL을 대상으로 합니다. 코드 스캐너는 열린 데이터베이스를 볼 수 없기 때문입니다.
- 터미널이 있는 코딩 에이전트는 12개 중 10개를 대신 실행해 줄 수 있습니다 (프롬프트는 아래에 있음). 그리고 그중 3개는 CI(지속적 통합) 단계에 포함되어야 합니다.
2026년 8월, 한 연구자가 Lovable, Bolt, v0, Replit으로 빌드된 약 31,000개의 라이브 앱을 대상으로 자동화된 점검을 실행했습니다. 이 점검이 도달할 수 있었던 Supabase 데이터베이스 중 57%는 로그인한 사람 여부와 관계없이 요청하는 모든 사람에게 데이터를 반환했으며 (Reeve, August 2026) 이는 기본적으로 인터넷에 열려 있었고 아무도 이를 비활성화하지 않았습니다.
제가 vibe-coded로 무언가를 배포한 첫 경험 때, Stripe 비밀 키 앞에 NEXT_PUBLIC_을 붙여서 자정쯤 'undefined' 오류를 수정했습니다. 오류는 사라졌지만, 키의 보안성도 사라졌습니다. 이 키는 9일 동안 제 앱을 로드하는 모든 브라우저 내부에서 평문(plain text)으로 자리 잡았습니다. 그러고 나서 아래 점검 2번을 실행했고, 단 하나의 grep 명령어가 그 내용을 저에게 곧바로 보여주었습니다. 키를 재변경하는 데 4분이 걸렸고, 이 키가 필요한 호출을 서버 라우트로 옮기는 데는 또 한 시간이 걸렸습니다. 제가 여기서 얻은 교훈은, 중요한 점검들은 내가 계속 읽어왔던 소스 코드가 아니라 빌드된 앱을 대상으로 한다는 것입니다.
12가지 점검 사항이 이어집니다. 각각은 명령어와 실패했을 때의 결과 모습, 그리고 수정에 걸리는 시간을 제공하며, 12개 중 6개는 이 게시물에서 보안 침해가 발생했던 곳인 소스 코드가 아니라 빌드된 앱 또는 라이브 URL을 대상으로 합니다.
바이브 코딩(Vibe-Coded) 앱이 유출되는 이유
AI가 작성한 코드는 사람이 작성한 코드만큼 안전합니다. 2026년 3월에 진행된 한 연구는 실제 저장소에 있는 수만 개의 AI 작성 코드 단위와 인간이 작성한 코드를 비교했으며, AI 코드가 스캐너 경고를 약간 적게 발생시키고 하드코딩된 비밀(hardcoded secrets)을 더 적게 포함하고 있다는 것을 발견했습니다 (Mao et al., March 2026).
데이터 유출은 앱이 무엇을 가지고 배포하는지에서 발생합니다. 올해 가장 명확한 예가 바로 Moltbook입니다. 이 앱은 2026년 1월 말에 한 창립자가 "코드를 단 한 줄도 작성하지 않았다"고 밝히며 출시한 AI 소셜 네트워크였습니다. 불과 4일 만에 Wiz의 연구원들은 사이트의 JavaScript에서 데이터베이스 키를 발견했고, 행 수준 보안(row-level security)이 비활성화되어 있어 누구나 전체 데이터베이스를 읽거나 쓸 수 있었습니다. 이로 인해 약 150만 개의 API 토큰과 수만 개의 이메일 주소가 노출되었습니다 (Wiz, February 2026).
같은 달에 Lovable이 자체 Discover 페이지에서 소개했던 한 시험 플랫폼에서는 18,697개의 사용자 기록이 유출되었습니다. 이 앱의 로그인 확인 기능은 역방향으로 작성되었고, 데이터베이스에는 행 수준 보안이 없었으며, 사용자가 어떤 역할을 가지고 있는지 검사하는 것도 아무것도 없었습니다. 이를 발견한 연구원은 주요 버그를 "인간 보안 검토자라면 몇 초 만에 잡아낼 전형적인 논리 반전(logic inversion) 오류"라고 지적했습니다 (The Register, February 2026). 두 달 후에는 플랫폼 자체에 모든 무료 계정이 다른 사용자의 프로젝트를 읽을 수 있게 하는 결함이 있었습니다.
이러한 이야기들은 공통적인 원인을 공유합니다. 앱을 사용자에게 선보이는 것은 예전에는 인증(authentication) 설정을 직접 구성해야 한다는 것을 의미했습니다. 왜냐하면 사용자가 설정하기 전까지는 아무것도 작동하지 않았기 때문입니다. AI 도구들이 그 단계를 제거했기 때문에, 앱은 기본 설정으로 계속 실행되며, 새로운 데이터베이스의 기본값은 '열림' 상태입니다. 이것이 이 체크리스트가 소스 코드보다는 배포된(deployed) 앱에 더 많은 시간을 할애하는 이유입니다.
체크리스트 사용 방법
첫 실제 가입 전에, 결제 처리를 하기 전에, 그리고 출시 후 한 달에 한 번씩 12가지 항목을 모두 실행하세요. 수동으로 실행할 필요는 없습니다. 마지막의 프롬프트를 코딩 에이전트에게 전달하면 전체 목록을 받고 보고서를 받게 됩니다.
명령어들은 Next.js App Router 프로젝트, npm, 그리고 Unix 셸(Windows: Git Bash 또는 WSL)을 가정합니다. 체크 3부터 5까지는 Supabase 전용입니다. 왜냐하면 Lovable, Bolt 및 대부분의 vibe-coding 도구들이 여러분에게 제공하는 것이 Supabase이며, 열린 Supabase 테이블은 개방 스캔 뒤에 있는 실패 지점이기 때문입니다. 만약 데이터베이스가 자체 API(Prisma, Drizzle, Neon) 뒤에 있다면 이 항목들을 건너뛰고 체크 7에 집중하세요. Firebase의 경우 동등한 실패는 모든 사람에게 읽기 권한을 허용하는 Firestore 규칙입니다. 체크 6과 7은 테이블 내 Authorization 섹션에서 Clerk의 이름(Auth.js, Better Auth 및 Supabase Auth)을 사용하며, 다른 패키지 관리자는 체크 8에 있습니다. Vite는 체크 2에 있습니다. app.example.com, abcdefghijkl.supabase.co와 같은 값들, 키와 ID들은 예시입니다. 사용자 자신의 것을 사용하고, 직접 타이핑하는 대신 DevTools(Network에서 요청을 마우스 오른쪽 버튼으로 클릭하여 cURL로 복사)에서 요청 URL과 쿠키를 가져오세요. 만약 앱이 Lovable 안에 있다면, 코드를 읽는 체크가 7개이기 때문에 먼저 GitHub로 내보내세요. 네 개의 스캐너는 별도의 설치 프로그램입니다. Homebrew에는 이 모든 것이 포함되어 있으며, Linux와 Windows에서는 각 프로젝트에서 릴리스 바이너리를 받습니다.
brew install gitleaks trufflehog osv-scanner semgrep
이 게시물에서 발생하는 피해 대부분은 소스 코드 외부에서 발생합니다. 데이터베이스 설정 문제, 배포된 파일에 포함된 키, 잘못된 사용자에게 응답하는 라우트 등이 그 예입니다. 저장소(repo)를 읽는 스캐너로는 이러한 것들을 볼 수 없습니다. 이것이 12가지 점검 사항 중 6개가 빌드 번들 또는 실제 URL과 통신하며, 코드 스캔이 마지막 순서로 배치된 이유입니다.
비밀 키: 세 가지 누출 지점 (Check 1–3)
키는 세 곳에서 유출됩니다. git 히스토리, 브라우저에 전송되는 JavaScript, 그리고 잘못된 종류의 데이터베이스 키입니다. 세 번째는 Supabase와 관련된 내용입니다.
AI 지원 도구들이 이 문제를 더욱 악화시키고 있습니다. GitGuardian의 2026년 보고서에 따르면 Claude Code로 작성된 커밋은 일반적인 공개 커밋보다 비밀 정보를 두 배가량 자주 유출하는 것으로 나타났습니다. (GitGuardian, March 2026). 같은 보고서는 수년 전에 유출된 대부분의 키들이 여전히 작동하고 있으며, 그 이유는 아무도 이들을 순환(rotate)시키지 않았기 때문이라고 밝혔습니다. 따라서 이 섹션의 모든 점검 사항에 대한 해결책은 동일하게 시작됩니다. 먼저 키를 순환시킨 다음, 정리하는 것입니다.
Check 1: git 히스토리에 비밀 정보가 있습니까?
gitleaks git . --redact -v
trufflehog git file://. --results=verified --fail
실패하는 경우: gitleaks가 발견 사항을 출력하고 코드 1로 종료될 때. 이전의 detect 서브커맨드는 사용 중단되었습니다. trufflehog 라인은 제공업체가 여전히 작동한다고 확인한 키들로 목록을 좁힙니다. 비용: 도구들은 무료입니다. 키를 순환시키는 데는 몇 분이 걸립니다. 이를 제거하기 위해 히스토리를 다시 작성하는 것은 약 한 시간이 걸리며, 순환시키는 것이 더 중요합니다. 왜냐하면 공개 커밋에 있는 키는 이미 복사되었기 때문입니다.
Check 2: 비밀 정보가 브라우저로 전송되었습니까?
npm run build && grep -rIoE \
-e 'sb_secret_[A-Za-z0-9_-]+' -e 'sk_(live|test)_[A-Za-z0-9]+' \
-e 'sk-[A-Za-z0-9_-]{20,}' -e 'postgres(ql)?://[^:/]+:[^@]+@' \
...```
실패하는 경우: 내용이 출력되는 모든 경우. Next.js는 `NEXT_PUBLIC_`로 시작하는 모든 변수를 빌드 시점에 평문(plain text)으로 브라우저 번들에 복사합니다. 따라서 누군가 `STRIPE_SECRET_KEY`에 접두사를 추가하여 "undefined" 오류를 수정하면, 해당 키가 공개됩니다. 이 패턴들은 Supabase 비밀 키, Stripe 및 Clerk의 라이브/테스트 키, OpenAI 키, 그리고 암호화된 데이터베이스 URL을 포함합니다. Vite에서는 폴더가 `dist/assets`이며 `VITE_` 값만 인라인 처리됩니다. **비용:** 무료. 키를 순환(Rotate)하고 해당 키가 필요했던 코드를 서버 라우트(server route)로 이동시키면 되며, 약 1시간이 소요됩니다.
### 체크 3: 어떤 Supabase 키를 배포했습니까?
grep -rhoE 'sb_(publishable|secret)[A-Za-z0-9-]+' .next/static | sort -u
grep -rhoE 'eyJ[A-Za-z0-9_-]{10,}.eyJ[A-Za-z0-9_-]{10,}' .next/static
| sort -u
...
**실패하는 경우:** 첫 번째 `grep`이 `sb_secret_` 키를 찾거나, 번들 내의 어떤 토큰이라도 디코딩된 페이로드(payload)가 `"role":"service_role"`이라고 명시하는 경우. Supabase에는 두 가지 종류의 키가 있습니다. 퍼블리셔블 키(publishable key)(과거 `anon`으로 불림)는 브라우저용이며 행 수준 보안(row-level security)에 의해 제한됩니다. 비밀 키(secret key)(과거 `service_role`)는 행 수준 보안을 완전히 무시하므로, 이를 배포하는 것은 전체 데이터베이스를 넘겨주는 것과 같습니다. 오래된 키들은 토큰(JWTs)입니다: 두 번째 `grep`은 번들 내의 모든 것을 나열하며, 노드 라인은 그중 일부의 중간 부분을 디코딩하여 역할을 읽을 수 있게 해줍니다. **비용:** 무료. 대시보드에서 순환하고 재배포하면 되며, 약 30분 정도 소요됩니다.
키가 작성되는 것을 막는 것이 나중에 순환하는 것보다 저렴합니다. Claude Code를 사용한다면, [10줄짜리 후크(ten-line hook)](https://vibeready.sh/blog/claude-code-primitives/?utm_source=devto&utm_medium=syndication&utm_campaign=vibe-coding-security-checklist#hooks-vs-skills)가 편집이 발생하기 전에 `.env`에 대한 수정을 거부합니다.
## 데이터베이스: 실제로 폐쇄되었습니까? (체크 4–5)
Supabase 앱의 경우 브라우저가 데이터베이스에 직접 연결하므로, 데이터베이스는 어떤 사용자가 어떤 행(row)을 읽을 수 있는지 결정해야 합니다. 이 작업은 row-level security (RLS)가 담당하며, SQL에서 생성하는 모든 테이블에는 기본적으로 활성화되어 있습니다. 개방된 테이블이란 오프닝 스캔에서 57%를 차지한 것입니다.
### 점검 4: RLS가 비활성화된 테이블은 무엇인가?
select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
...
**실패하는 경우:** 어떤 행이라도 `rowsecurity = f`로 표시될 때. 해당 테이블은 공개 키(publishable key)를 가진 누구나 읽고 쓸 수 있습니다. 이 키는 브라우저 번들 안에 있기 때문입니다. 그런 다음 Supabase 대시보드에서 [Security Advisor](https://supabase.com/docs/guides/database/database-advisors)를 열어 `0013_rls_disabled_in_public`, `0010_security_definer_view`, `0002_auth_users_exposed` 및 `0024_permissive_rls_policy` (USING (true)라고 명시하는 정책)에 대해 먼저 조치를 취하십시오. **비용:** 무료입니다. 첫 번째 테이블을 처리한 후에는 정책 작성에 테이블당 10분에서 30분이 걸립니다.
### 점검 5: 외부에서 증명하기
curl "https://abcdefghijkl.supabase.co/rest/v1/orders?select=*&limit=5"
-H "apikey: sb_publishable_AbC123..."
**실패하는 경우:** 실제 행이 출력될 때. 프로젝트 URL과 공개 키는 Supabase 대시보드의 Settings > API에 있으며, 테이블은 사용자의 모든 테이블일 수 있습니다. 행이 있다고 알려진 테이블에서 빈 배열이 나오는 것은 RLS가 작동하고 있다는 의미이며, 코드 42501을 가진 오류 객체는 익명 역할(anonymous role)이 해당 테이블에 전혀 접근할 수 없다는 뜻이므로 이 역시 괜찮습니다. 사용자 데이터를 담고 있는 모든 테이블에 대해 반복하십시오. 이는 Moltbook의 데이터베이스와 Reeve 스캔에서 발견된 개방된 데이터베이스를 찾아낸 것과 동일한 요청입니다. **비용:** 수정 비용은 점검 4와 같습니다.
만약 자체 API 라우트 뒤에서 Prisma를 사용한다면, RLS는 적용되지 않으며 이 두 가지 점검은 스스로 통과합니다. 귀하의 버전은 점검 7입니다. Lovable 관련 실패 사례에는 논란이 되는 2025년 취약점 보고서(CVE)가 포함되는데, 이는 Lovable이 벽에 부딪히는 부분에서 다루어집니다.
## 권한 부여: 기록의 소유자는 누구인가? (점검 6–7)
접근 통제 오류(Broken access control)는 [OWASP Top 10:2025](https://top10.owasp.org/2025/)에서 최상위 항목입니다. 간단히 말해, 앱이 사용자가 로그인했는지 확인한 후, 그 기록이 사용자 본인의 것인지 확인하지 않고 그냥 제공하는 경우를 의미합니다. 이것이 올해 Lovable 사고 두 건 모두의 버그였습니다.
| Provider | route 핸들러 내 인증 호출 (Auth call) | 세션 쿠키 (Session cookie) |
| :--- | :--- | :--- |
| **Clerk** | `auth()` 또는 `auth.protect()` | `__session`, 복사 후 약 60초 만료 |
| ... |
### 점검 6: 인증을 전혀 확인하지 않는 라우트 핸들러는 몇 개인가?
find app src/app -name 'route.' 2>/dev/null | wc -l
grep -rlE 'auth()|protect(|getSession(|getClaims(|getUser('
--include='route.' app src/app 2>/dev/null | wc -l
자체 API 라우트가 있는 앱에만 해당합니다. 만약 앱이 Supabase와 직접 통신한다면, 권한 부여는 RLS(Row Level Security)이며, 점검 4, 5, 7이 이를 다룹니다. **실패하는 경우:** 두 번째 숫자가 첫 번째 숫자보다 (자신의 인증 제공업체의 모든 라우트 및 웹훅 수신기를 제외하고) 1개 이상 작을 때입니다. 이 표는 각 제공업체별 인증 호출을 명시합니다. Clerk의 경우, `clerkMiddleware()`만으로는 아무것도 보호하지 못하며, [문서](https://clerk.com/docs/references/nextjs/clerk-middleware)에 따르면 미들웨어는
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기