AI 코드 보안 점검 루틴 정리했다 — 반복되는 7가지 패턴
Apsity·FeedMission을 비롯해 만진 코드 절반 이상이 AI 생성이다. 운영하다 보면 동일한 종류의 보안 구멍이 반복된다는 걸 알게 됐다. 한 번의 점검에서 크리티컬 7건이 같이 나왔던 사례, 그 7건이 통계상 가장 흔한 패턴이라는 사실, 그리고 지금 내가 배포 직전 항상 돌리는 10분 점검 루틴을 정리했다.
On this page (9)
2026년 4월 · 귀찮은개발자 EP.18
AI 코드 보안 점검 루틴 정리했다 — 반복되는 7가지 패턴
Apsity 앱스토어 대시보드, FeedMission SaaS, 사이드 프로젝트 십수 개 — 만진 코드 절반 이상이 AI 생성이다. SaaS 한 개를 7일 만에 출시한 이후로(EP.04) 바이브코딩이 기본 워크플로가 됐다.
오래 하다 보니 패턴이 보였다. AI가 짠 코드에는 동일한 종류의 보안 구멍이 반복된다. FeedMission 한 번 점검에서 크리티컬 7건이 한꺼번에 나온 적이 있다(EP.06) — 프론트 번들에 박힌 Slack 웹훅 URL, 이메일만 알면 누구나 해지되는 구독 엔드포인트, 공개 API 응답으로 새던 관리자 답변 전문, 팀 멤버 권한 체크가 빠진 라우트들. 그 7건은 "운 나쁘게 모인" 게 아니다. 업계 분석에서 통계적으로 가장 자주 나오는 패턴이 거의 그대로 우리 코드에 들어와 있었다.
그래서 지금은 패턴화된 7가지를 배포 직전에 항상 같은 방식으로 점검한다. 이 글은 그 패턴 카탈로그와 점검 루틴이다. 바이브코딩 입문자라면 같은 7가지 중 최소 3개는 지금 본인 리포에 있다고 봐도 된다. 통계상 그렇다.
- AI 생성 코드 취약점 비율: 40~62% · 사람 코드 대비 2.74배 더 위험 (2026 업계 조사)
- 실수 TOP 7: 하드코딩 시크릿, 인증 없는 API Route, NEXT_PUBLIC_ 오용, SQL 문자열 보간, CORS 와일드카드, XSS·log injection 누락, 유령 패키지(slopsquatting)
- FeedMission 사례: 한 번 점검에서 크리티컬 7건 발견 — 이게 누구에게나 일어난다
- 10분 점검 루틴: grep 3줄 + AI 셀프 리뷰 + Secret Scanning + Push Protection
- 하네스(EP.17)에 보안 단계를 박으면 잊어버릴 수가 없다
- Supabase 사용자 추가 항목: RLS 미활성 / WITH CHECK 누락 / service_role 노출 — 마지막 보너스 섹션에서 점검 SQL까지
먼저 숫자 — 체감보다 심각하다
"설마 내 코드만 그렇겠어"라고 생각하지 마라. 체감이 아니라 측정된 숫자다. 2026년 여러 기관(Georgia Tech, Cloud Security Alliance, Checkmarx)이 발표한 AI 생성 코드 분석에서 40~62% 샘플에 보안 취약점이 있었다. 사람이 쓴 코드 대비 약 2.74배 더 위험하다는 결과도 나왔다.
더 구체적으로는, 분석된 샘플 중 86%가 XSS 방어에 실패했고, 88%가 로그 인젝션에 취약했다. 2026년 3월 한 달에만 AI 생성 코드로 발생한 CVE 35건이 새로 추적됐다. 어떤 AI 앱 하나는 출시 직후 1.5M 개 API 키가 유출됐다. 보안 리뷰 없이 그대로 배포해서다.
알고 있다. 이 숫자 봤다고 바이브코딩을 안 할 사람은 없다. 나도 계속 쓴다. 다만 "배포 전 10분"이 있느냐 없느냐가 프로덕션 운명을 가른다.
AI가 보안 코드를 미루는 방식
입문자들이 가장 오해하는 지점이다. AI가 실수한 게 아니라, AI는 요청받은 대로 만들었다. "유저 프로필 API를 만들어줘"라고 시키면 유저 프로필 API를 만든다. 인증은 요청하지 않았으니 안 붙인다. 그 자리에 // TODO: add auth here 한 줄 남겨두고 넘어간다.
기능이 동작하면 완성이라고 느낀다. 테스트도 통과한다. 하지만 TODO 주석 한 줄이 공격 포인트가 된다. URL만 알면 누구나 호출할 수 있는 엔드포인트가 그대로 프로덕션에 올라간다. 기능 관점에선 완벽한 코드가, 보안 관점에선 구멍투성이다.
해결은 간단하다. 프롬프트에 보안 조건을 처음부터 써라. "JWT 인증 미들웨어 포함, 시크릿은 env에서만 참조, raw SQL 금지, TODO 주석 없이 완성해라" — 이 한 줄이 생성 품질을 바꾼다. 조건이 있으면 AI는 그 조건에 맞는 코드를 만든다.
실수 TOP 7 — 내가 직접 밟아본 순서대로
아래는 내가 FeedMission·Apsity 운영 중에 직접 밟았거나 리뷰 중에 매번 발견한 패턴이다. 빈도 순으로 나열한다. 바이브코딩 입문자라면 이 중 최소 3개는 지금 내 리포에 있다고 봐도 된다.
| # | 실수 패턴 | 무엇이 일어나나 | 점검 신호 |
|---|---|---|---|
| 1 | 하드코딩된 API 키 | 커밋 순간 봇이 수초 안에 수집 | sk_, api_key= 문자열 |
| 2 | 인증 없는 API Route | URL 알면 누구나 DB 접근 | session/auth/token 키워드 없음 |
| 3 | NEXT_PUBLIC_ 오용 | 서비스 롤 키가 브라우저 번들에 평문 | NEXT_PUBLIC_*_SECRET/KEY |
| 4 | raw SQL 문자열 보간 | SQL 인젝션 → DB 전체 털림 | `SELECT ... ${}` |
| 5 | CORS 와일드카드 | 아무 도메인에서 API 호출 가능 | Allow-Origin: * |
| 6 | XSS·log injection 방어 누락 | 사용자 입력 그대로 HTML/로그 주입 | dangerouslySetInnerHTML, 원문 로그 |
| 7 | 유령 패키지 (slopsquatting) | AI가 추천한 가짜 이름으로 악성 패키지 설치 | 다운로드 적은 낯선 패키지 |
특히 1번과 3번은 배포 직후 가장 많이 털린다. GitHub에 올리는 순간 자동 스캐너가 키를 수집해서 코인 채굴에 쓰거나 API 한도를 소진시킨다. 체감한 적 없으면 그저 운이 좋았을 뿐이다.
3번에 대한 한 가지 보충: Supabase 같은 BaaS를 쓰면 NEXT_PUBLIC_SUPABASE_ANON_KEY는 의도적으로 클라이언트에 노출되도록 설계돼 있다. 노출 자체는 정상이다. 단, 그게 안전한 이유는 RLS(Row Level Security)가 켜져 있다는 전제 위에서다. RLS 꺼져 있으면 anon key는 사실상 service role과 같은 권한을 갖는다. 자세한 점검법은 마지막 보너스 섹션에 따로 정리했다.
AI가 npm install some-plausible-package라고 하면 일단 npmjs.com에서 다운로드 수·메인테이너·issues를 확인해라. AI 생성 코드 약 20%가 실존하지 않는 패키지명을 참조한다는 조사가 있다. 공격자가 그 이름을 먼저 등록해서 악성 코드를 심으면 그대로 install된다. 이름이 낯설면 "이거 진짜 있는 패키지야?"를 AI한테 다시 물어보는 것도 방법이다.
FeedMission에서 직접 털렸으면 어땠을까 — EP.06 케이스
위 7개 중 FeedMission에 있었던 건 2, 3, 6, 그리고 앱 고유 결함 몇 개였다. 구체적으로:
- Slack 웹훅 URL이 ProjectContext를 타고 프론트 번들에 포함됐다. 공격자가 내 워크스페이스로 가짜 피드백을 무제한으로 쏠 수 있었다.
- 구독 해지 API가 이메일 주소만 받았다. 누구 이메일이든 넣으면 해지가 됐다.
unsubscribeToken기반으로 바꿨다. /api/feedback/mine가 관리자 답변 전문을 그대로 반환했다.hasReply: boolean만 돌려주도록 교체했다.- 팀 멤버 권한 체크가 여러 API에서 빠져있었다. 소유자 ID만 비교해서 공동 작업자가 접근 못 하거나, 반대로 누구나 접근되는 엔드포인트가 생겼다.
.env가.vercelignore에 없어서 심볼릭 링크로 Vercel 빌드에 올라갈 뻔했다.
이걸 한 번에 잡은 커밋이 있다(52efb89). 그 뒤로 비슷한 실수를 반복하지 않으려고 루틴을 만들었다. 어느 하나도 "너무 특이해서 나한텐 안 생길" 실수가 아니다. 바이브코딩으로 SaaS 만들면 비슷한 조합이 반드시 나온다.
내가 쓰는 10분 점검 루틴
거창한 도구는 없다. 셸 스크립트 한 줄, AI 셀프 리뷰 한 번, GitHub 설정 세 개. 이것만 습관이 되면 위 7개 중 6개는 자동으로 걸러진다.
# 미완성 보안 코드
grep -r "TODO\|FIXME\|implement.*later\|add.*auth" ./src
# 하드코딩된 시크릿
grep -r "sk_\|api_key\|password\s*=" ./src
# 클라이언트 노출 환경변수
grep -rE "NEXT_PUBLIC_.*(SECRET|KEY|TOKEN)" ./src
# 2. SQL 문자열 보간과 CORS 와일드카드도 같은 방식으로
grep -rn "\`SELECT\|\`INSERT\|\`UPDATE\|\`DELETE" ./src
grep -rn "Allow-Origin.*\*" ./src
셋 다 통과하면 그 다음은 AI 셀프 리뷰다. 방금 받은 코드를 그대로 프롬프트에 붙이고 "OWASP Top 10 기준으로 이 코드의 취약점을 리뷰해줘" 한 줄 던지면 된다. 완벽하지 않다. 자기가 만든 버그를 못 잡는 경우도 있다. 그래도 1차 필터로는 충분하다.
GitHub 쪽은 세 가지만 켜면 된다: Secret Scanning, Push Protection, CodeQL Code Scanning. 이 조합이 커밋 시점에서 키 유출을 막고, push 시점에서 코드 레벨 취약점을 잡는다. Dependabot·npm audit을 CI에 걸면 패키지 취약점도 자동 추적된다.
코드 생성 요청 맨 끝에 이 한 줄을 항상 붙인다: "인증 미들웨어 포함, 시크릿은 process.env에서만 읽고 NEXT_PUBLIC은 공개값에만 사용, 사용자 입력은 반드시 검증, raw SQL 금지, TODO/FIXME 없이 완성해라." 결과 품질이 눈에 띄게 달라진다. 바이브코딩이라고 프롬프트 짧게 쓸 이유 없다.
EP.17 하네스에 박아 넣으면 잊을 수가 없다
문제는 사람이라는 거다. 바쁘면 점검을 건너뛴다. 핫픽스 급하게 올릴 때 grep 한 줄이 귀찮아진다. 그래서 나는 EP.17 하네스의 /verify 단계에 보안 체크를 박아뒀다. /workflow "기능명"만 돌리면 설계 → 구현 → 보안 grep → AI OWASP 리뷰 → 커밋이 한 흐름으로 돈다.
잊어버릴 수가 없다. 잊어버려도 하네스가 알려준다. 이게 내가 이 시리즈에서 계속 말하는 "귀찮은 건 자동화"의 진짜 의미다. 보안 점검은 특히 귀찮다. 그러니까 가장 먼저 자동화해야 한다.
.claude/commands/verify.md에 다음을 박아둔다: (1) grep 3줄 실행, (2) AI에게 OWASP Top 10 관점 리뷰 요청, (3) 환경변수 목록과 인증 미들웨어 존재 여부 확인, (4) 취약점 발견 시 즉시 /tdd로 되돌아감. 이 4단계가 실행 안 되면 /commit이 막히도록 한다.
보너스 — Supabase 쓰면 RLS는 따로 챙겨라
Next.js + Supabase는 바이브코더 기본 스택이다. 그래서 RLS 한 챕터를 따로 둔다. RLS(Row Level Security)는 PostgreSQL이 기본 제공하는 행 단위 접근 제어 기능이다. "이 행은 user_id가 일치하는 사람만 읽을 수 있다"를 DB 레벨에서 강제한다.
왜 별도 섹션이 필요한가. Supabase Studio에서 새 테이블을 만들면 RLS는 기본 OFF다. 이 상태에서 NEXT_PUBLIC_SUPABASE_ANON_KEY를 그대로 클라이언트에 노출하면, 그 키 하나만 알면 누구나 모든 테이블의 모든 행에 접근할 수 있다. anon key가 사실상 service role과 동일한 효력을 갖는다. 클라이언트 노출이 안전하다는 보장이 무너진다.
RLS를 켰다고 끝이 아니다. 정책(Policy)을 작성하지 않으면 모든 접근이 차단된다. 그래서 보통 SELECT, INSERT, UPDATE, DELETE 각 동작별로 정책을 따로 만든다. 가장 흔한 실수는 USING(읽기·삭제 시점 필터)은 썼는데 WITH CHECK(쓰기 후 검증)를 빠뜨리는 것이다. 그러면 user_a가 user_b의 user_id로 INSERT/UPDATE를 시도해서 남의 행을 만들거나 갈취할 수 있다.
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public' AND rowsecurity = false;
-- 2. 정책 한 줄도 없는 테이블 찾기 (RLS만 켜놓고 정책 없으면 전부 거부됨)
SELECT t.tablename
FROM pg_tables t
LEFT JOIN pg_policies p
ON t.schemaname = p.schemaname AND t.tablename = p.tablename
WHERE t.schemaname = 'public' AND p.policyname IS NULL;
-- 3. WITH CHECK 빠진 INSERT/UPDATE 정책 점검
SELECT tablename, policyname, cmd, qual, with_check
FROM pg_policies
WHERE schemaname = 'public' AND cmd IN ('INSERT', 'UPDATE') AND with_check IS NULL;
Supabase SQL Editor에 이 세 쿼리를 저장해두고 마이그레이션마다 돌리는 게 가장 빠르다. 첫 쿼리 결과가 비어있으면 모든 테이블에 RLS가 켜진 거다. 두 번째가 비어있으면 정책이 최소 1개씩은 있다. 세 번째가 비어있으면 INSERT/UPDATE에 WITH CHECK가 다 들어가 있다.
- RLS 미활성 — 새 테이블 만들고 ON 안 함. anon key가 곧 마스터키.
- WITH CHECK 누락 — USING만 있고 WITH CHECK 없음. 남의 행을 내 user_id로 위장해서 INSERT 가능.
- service_role 키 클라이언트 노출 —
SUPABASE_SERVICE_ROLE_KEY는 절대 NEXT_PUBLIC 안 됨. 서버 라우트나 Edge Function에서만 참조. - auth 없는 anon role에 과한 권한 — 정책에
auth.uid() = user_id없이true로 열어두면 비로그인 사용자도 모든 행 접근 가능.
Supabase 외 BaaS도 비슷하다. Firebase는 Security Rules, Appwrite는 Permissions, PocketBase는 Collection rules. 이름만 다르고 원리는 같다 — "클라이언트가 직접 DB를 부른다 = DB가 마지막 방어선이다". 이 방어선을 비워두면 앞단에 무슨 보안을 둬도 의미 없다.
FAQ
Q. AI 코드가 진짜로 사람 코드보다 위험한가?
업계·학계 공동 연구에서 AI 생성 코드는 사람 코드보다 약 2.74배 더 취약점이 많다고 보고됐다. 40~62% 샘플에 보안 구멍이 있었다. 프롬프트에 보안 조건을 쓰지 않으면 AI는 기능 구현만 한다.
Q. 바이브코딩 입문자가 가장 자주 밟는 실수는?
하드코딩된 API 키 커밋, 인증 없는 API Route, NEXT_PUBLIC_ 잘못 붙이기, 존재하지 않는 패키지 설치(slopsquatting), raw SQL 문자열 보간 — 이 다섯 개가 압도적으로 많다.
Q. 슬롭스쿼팅(slopsquatting)은 뭔가?
AI가 존재하지도 않는 패키지명을 추천하는 현상을 노려, 공격자가 그 이름으로 악성 패키지를 먼저 올리는 공격이다. AI 생성 코드의 약 20%가 유령 패키지를 참조한다는 조사가 나왔다. AI가 추천한 패키지가 낯설면 npmjs.com에서 다운로드 수·이슈·메인테이너를 확인해라.
Q. .env가 이미 깃에 올라갔다면?
그 시점의 키는 이미 유출됐다고 가정해라. 즉시 키를 폐기하고 새로 발급한다. git rm으로 지워도 히스토리에 남는다. GitHub Push Protection을 켜두면 다음에는 커밋 시점에서 차단된다.
Q. 보안 점검을 자동화하려면?
GitHub Secret Scanning + CodeQL + npm audit을 GitHub Actions에 넣으면 push마다 자동 실행된다. 개인 계정 신규 공개 리포는 Secret Scanning이 기본 켜져 있고, 비공개·조직 리포는 Settings에서 활성화한다. 이 세 개 + grep 한 줄이 내 최소 루틴이다.
마무리
바이브코딩 때문에 보안이 나빠진 게 아니다. 점검 없이 배포하는 습관이 나빠진 거다. 속도는 AI가 올렸다. 점검 속도도 같이 올리면 된다. grep 세 줄, AI 리뷰 한 번, GitHub 설정 세 개. 10분이면 된다.
그 10분을 안 쓰면 1.5M 개 API 키가 유출되는 게 먼 나라 이야기가 아니다. 내 이야기가 된다. 반대로 10분을 쓰면 바이브코딩의 속도 그대로, 기본 방어선은 유지한 채 배포할 수 있다. 선택은 둘 중 하나다. 입문자일수록 쉬운 쪽이 아니라 안전한 쪽을 선택해야 한다.
GoCodeLab 블로그
AI 소식과 개발 자동화 이야기를 매주 올린다
이 글은 2026년 4월 기준이다. 업계 통계는 Georgia Tech Vibe Security Radar, Cloud Security Alliance, Checkmarx 발표 수치를 참조했다. 도구·플랫폼 정책은 변경될 수 있으니 구체적 조치는 해당 서비스 공식 문서를 함께 확인하기 바란다.
최종 업데이트: 2026년 4월 13일 · GoCodeLab