샘플 리포트

출시 점검 리포트

얼마나 흔한가
공개 애플리케이션 1,003개 중 593개(59.1%)에 출시 전에 확인해야 할 부류가 하나 이상 잡혔습니다. 두 가지가 겹칠 때만 성립하는 결론은 146개(14.6%)에서 나왔습니다. 아래 항목별 수치는 그보다 작은 표본을 손으로 열어본 값이라 따로 적습니다
이 문서
고객 검사에서 받으시는 리포트와 같은 형식입니다. 대상 앱 하나가 아니라, 공개 애플리케이션 1,003개 검사에서 실제로 나온 결함 종류를 고칠 순서대로 놓았습니다
근거
넓이는 공개 애플리케이션 1,003개(측정일 2026-08-17), 깊이는 Next.js + Supabase 앱 87개와 Firebase 앱 60개를 손으로 열어본 기록(측정일 2026-08-15) · 검사 기록
표본
Supabase 쪽은 별 21개 이상, 중앙값 66개, 최대 5,766개. 전부 2025년 6월 이후 갱신됐습니다. 사람들이 실제로 참고하고 복제하는 코드이지 방치된 개인 실험이 아닙니다
검사 범위
소스코드, 의존성, 설정 파일, DB 스키마, 접근 규칙 파일, 컨테이너·서버 설정
검사 도구
시크릿 검사 · 의존성 검사 · 스키마·코드 패턴 대조 · 코드 검토

판정

보류 — 지금 이대로 출시하면 안 됩니다.

이미 운영 중이라면 오늘 안에 A·B·C·D를 처리하고, 그전에 대시보드 조치 3건을 먼저 하십시오.

차단 항목
A · B · C · D — 넷 다 지금 이 순간 실행 가능하고, 각각 데이터·금전·권한에 직접 닿습니다
해제 조건
네 항목의 “고쳤는지 어떻게 확인하나”가 전부 통과. E·F는 출시를 막지 않습니다
판정 근거
B는 인증 없이 데이터베이스 전체에 닿고, A는 개인정보 유출로 법정 신고 의무까지 이어집니다
검토
스캐너 2종과 스키마·코드 패턴 대조를 돌린 뒤, 코드를 읽고 판정했습니다

한 페이지 요약

이 앱이 이미 운영 중이라면 지금 이 순간 세 가지가 가능합니다. 아무나 접근 규칙이 빠진 테이블의 내용을 볼 수 있고, 돈을 내지 않고 주문을 완료할 수 있으며, 저장소에 남은 키로 서비스를 대신 쓸 수 있습니다.

발견
6
오늘 안에
4
이번 주
2

코드를 고치기 전에 대시보드에서 할 조치가 3건 있습니다. 아래 “지금 10분 안에”.

같은 87개에 시크릿 검사와 의존성 검사를 돌렸습니다. 두 검사가 내놓은 것은 D와 F뿐입니다. A·B·C·E는 둘 다의 탐지 대상이 아니고, 스키마와 코드를 대조해야 나옵니다.

공개 정적 분석 룰셋 룰 219개와 보안 룰셋 7종(룰 135개)을 같은 결함에 돌려본 별도 측정 기록도 있습니다. 결과는 같았고, 요청하시면 그대로 보내드립니다. 두 가지를 함께 알려드립니다. 룰을 직접 쓰면 잡히는 항목도 있습니다. 못 잡는 것은 도구가 아니라 기본 설정입니다. 그리고 그 측정은 파일 11개짜리 작은 앱에서 한 것이라 규모가 작아 도구에 불리했다는 반론이 가능하며, 저희도 그 반론이 타당하다고 봅니다.

위 87개 수치는 스캐너 출력이 아니라 걸러낸 뒤의 값입니다. 시크릿은 원시 검출 23곳에서 문서·예시·픽스처를 걷어내 11곳이 됐고, 그중 4곳을 직접 열어 진짜 유출 1건을 확인했습니다. 접근 규칙은 파싱 부산물과 Supabase 관리 스키마, 표기만 다른 같은 테이블을 걷어낸 뒤 6곳·50개가 남았습니다. 걷어내는 일이 찾는 일보다 오래 걸립니다. 고객 검사에서도 같은 순서를 지킵니다.

지금 10분 안에

코드를 건드리지 않고 대시보드에서 끝나는 일입니다. 코드만 고치면 이미 밖으로 나간 것은 그대로 살아 있으므로, 이 셋이 먼저입니다.

  1. 1 · 약 2분

    노출된 키를 회전시키십시오

    Supabase 대시보드 → Settings → API → service_role 키 Reset. 저장소에 커밋된 키가 더 있으면(아래 D) 해당 서비스 대시보드에서도 같이 회전시키십시오. 그다음 배포 플랫폼의 환경변수를 새 값으로 바꾸십시오.

    이 회전은 지금 배포된 앱을 즉시 멈춥니다. 앱이 이 키로 데이터베이스에 붙고 있기 때문입니다. 그래도 먼저 하시길 권합니다. 키는 이미 나간 상태이고, 코드 수정과 배포까지 걸리는 시간 동안 데이터베이스가 열려 있는 것보다 잠깐 멈추는 편이 낫습니다.

    사이트를 멈출 수 없는 상황이라면 순서를 바꾸십시오. 아래 B의 수정 지시를 먼저 적용해 anon 키로 도는 코드를 배포하고, 배포가 끝난 직후에 회전합니다. 이 경우 배포가 끝날 때까지는 예전 키가 계속 유효하다는 것을 알고 계셔야 합니다. 어느 쪽을 택하든 회전 자체를 건너뛰면 코드를 고쳐도 소용이 없습니다.

  2. 2 · 약 3분

    AI 제공사의 월 예산 상한과 알림을 켜십시오

    대시보드 → Settings → Limits에서 월 예산 상한과 이메일 알림을 설정하십시오. 현재 이 앱은 호출 상한이 없어 자동 반복 호출로 요금이 계속 올라갑니다(아래 E). 코드 수정 전까지는 이게 유일한 방어선입니다.

  3. 3 · 약 3분

    호스팅의 지출 상한을 켜십시오

    배포 플랫폼 대시보드 → Settings → Spend Management에서 상한과 알림을 켜십시오. 위 2번은 AI 호출료만 막습니다. 호스팅은 내보낸 전송량으로 따로 과금하고, 공격 트래픽도 전송량입니다. Vercel은 DDoS 트래픽을 포함한 모든 전송량을 GB당 $0.15에 매기며 지출 상한은 기본값이 아닙니다. 출처

    상한에 걸리면 사이트가 멈출 수 있습니다. 금액을 정하실 때 지난달 실제 전송량을 먼저 보시고, 알림 임계를 상한보다 낮게 두십시오.

오늘 안에

코드를 고쳐야 하는 항목입니다. 지금 이 순간 누구나 실행할 수 있으니 하루를 넘기지 마십시오.

A 심각

일부 테이블에만 접근 규칙이 걸려 있습니다

무슨 일이 일어나나

데이터베이스에 “누가 어떤 줄을 볼 수 있는가” 규칙이 테이블마다 따로 걸립니다. 어떤 테이블에는 걸려 있고 어떤 테이블에는 없습니다. 규칙이 없는 테이블은 브라우저에 공개된 키만으로 전부 조회됩니다. 그 안에 연락처나 주문이 있으면 개인정보 유출이고, 법정 신고 의무로 이어집니다.

누가, 어떻게 할 수 있나

필요한 것
브라우저에 공개된 anon 키. 앱을 열면 누구나 가집니다
난이도
브라우저 개발자도구에서 요청 한 줄
영향 범위
규칙이 빠진 테이블의 전체 행

어떻게 확인했나

스키마 파일에서 create table 문과 enable row level security 문을 테이블 단위로 대조합니다. 표기가 다른 같은 테이블은 하나로 셉니다. Firebase 앱이면 규칙 파일에서 조건이 true이거나 로그인 여부만 보는 규칙을 찾습니다. 시크릿 스캐너와 의존성 스캐너의 탐지 대상이 아닙니다

Supabase 스키마 파일

공개 앱 87개 검사에서 가장 많이 나온 형태입니다. SQL로 테이블을 만드는 60곳 중 6곳이 여기에 해당했고, 그 6곳에 규칙 없는 테이블 50개가 남아 있었습니다. 가장 선명한 곳은 테이블 82개에 규칙을 걸고 3개를 빠뜨렸습니다. 82번 제대로 한 사람이 3번 놓쳤습니다. “대시보드에서 켰겠지”라는 설명이 이 형태에는 통하지 않습니다 — 같은 파일에서 다른 테이블에는 켰기 때문입니다. 한 회사의 문제가 아닙니다. 같은 날 공개 Firebase 앱 60개를 따로 검사했더니, 규칙 파일을 저장소에 둔 25곳 중 8곳에 조건이 true인 규칙이 있었고 3곳은 쓰기까지 열려 있었습니다. 4곳은 계정만 있으면 전체에 닿습니다. 같은 60개 안에 범위가 제대로 지정된 규칙이 302개 있습니다. 다만 저장소의 규칙 파일이 실제로 배포된 규칙이라는 보장은 없어서, Firebase 수치는 저장소에 무엇이 들어 있는가까지만 말합니다.

create table public.orders (...);
alter table public.orders enable row level security;

create table public.contacts (...);
-- 세 줄 위에는 있는 규칙 문이 여기에는 없습니다

어떻게 고치나

규칙을 켜는 문만 먼저 넣지 마십시오. 정책 없이 규칙만 켜지면 모든 조회가 빈 결과가 되고 앱이 그 자리에서 멈춥니다. 아래 지시가 둘을 같은 파일에 넣는 이유입니다.

아래를 Claude Code·Cursor·Codex에 그대로 붙여넣으십시오.

Supabase 스키마에서 접근 규칙이 빠진 테이블을 찾아 규칙과 정책을 함께 넣어줘.

중요: 규칙을 켜는 문과 정책을 반드시 같은 마이그레이션 파일에 넣어줘.
정책 없이 규칙만 켜진 순간 모든 조회가 빈 결과가 되고 앱이 멈춘다.

작업:
1. create table은 있는데 enable row level security가 없는 테이블을 전부 나열.
   표기가 섞여 있으면 public.x와 x를 같은 테이블로 본다
2. 각 테이블에 소유자 조건 정책 추가 — 로그인 사용자가 자기 행만 select/insert/update
3. 공개로 읽혀야 하는 테이블은 규칙을 끄지 말고 읽기 전용 정책으로 연다
4. anon 역할에 남아 있는 불필요한 GRANT를 회수
5. auth·storage 같은 Supabase 관리 스키마는 건드리지 않는다

유지해야 할 동작: 로그인 사용자의 현재 화면과 API 응답은 동일해야 한다
변경 금지: 애플리케이션 로직, 기존 컬럼 구조

확인: 로그인하지 않은 요청과, 사용자 A 토큰으로 사용자 B의 행 조회 → 둘 다 빈 배열

고쳤는지 어떻게 확인하나

로그인 없이 조회 → 빈 결과. 사용자 A로 사용자 B의 행 조회 → 빈 결과. 그리고 스키마 파일에서 create table 수와 enable row level security 수를 세어 같은지 보십시오. 이 검사에서 걸린 6곳은 전부 두 수가 달랐습니다.

B 심각

관리자 키를 브라우저로 나가는 파일에서 씁니다

무슨 일이 일어나나

데이터베이스의 모든 규칙을 무시하는 마스터 키가 있습니다. 그 키를 참조하는 코드가 “브라우저에서 실행”으로 표시된 파일에 들어 있습니다. 이 파일은 웹페이지 소스에 실려 나갑니다.

누가, 어떻게 할 수 있나

필요한 것
변수 이름이 NEXT_PUBLIC_로 시작하면 아무것도 필요 없습니다. 페이지 소스를 열면 보입니다
난이도
브라우저에서 페이지 소스 보기
영향 범위
이름이 공개형이면 데이터베이스 전체 — 읽기·수정·삭제. 아니면 그 기능이 권한 없이 조용히 실패합니다

어떻게 확인했나

코드를 읽고 발견. “브라우저에서 실행” 표시와 마스터 키 참조가 같은 파일에 있는지 대조합니다

“use client”가 붙은 모듈

두 경우 다 고쳐야 합니다. 변수 이름이 NEXT_PUBLIC_로 시작하면 Next.js가 그 값을 번들에 그대로 박아 넣으므로 키가 밖으로 나갑니다. 붙어 있지 않으면 브라우저에서 값이 비어 있어, 관리자 권한이 필요한 동작이 권한 없이 도는 코드가 됩니다. 앞은 유출이고 뒤는 조용한 고장입니다.

'use client'
import { createClient } from '@supabase/supabase-js'

export const admin = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!   // ← 브라우저로 가는 파일입니다
)

어떻게 고치나

키 회전은 위 “지금 10분 안에”에 있습니다. 회전은 배포된 앱을 즉시 멈추므로, 그 항목의 순서 설명을 먼저 읽으십시오.

아래를 Claude Code·Cursor·Codex에 그대로 붙여넣으십시오.

service_role 키가 'use client' 모듈에서 참조되고 있어. 서버 전용으로 옮겨줘.

작업:
1. 'use client'가 붙은 파일에서 service_role 참조를 전부 제거
2. 브라우저용 클라이언트는 anon 키만 쓰도록 분리 (lib/supabase-client.ts)
3. 서버 전용 클라이언트를 만들고 SUPABASE_SERVICE_ROLE_KEY (NEXT_PUBLIC_ 없이) 사용
   → lib/supabase-server.ts, 서버 컴포넌트와 API route에서만 import
4. 관리자 권한이 필요한 동작은 API route로 옮기고, 그 안에서 호출자를 먼저 인증

유지해야 할 동작: 현재 화면과 API 응답이 동일해야 한다
변경 금지: 데이터베이스 스키마, 비즈니스 로직

확인: npm run build 후 grep -r "service_role" .next/static → 결과 없음

고쳤는지 어떻게 확인하나

고치기 전에 npm run build 후 grep -r "service_role" .next/static을 먼저 돌려보십시오. 지금 검색되면 키가 이미 나가 있는 것입니다. 수정 후 같은 명령이 아무것도 찾지 못해야 합니다. 그리고 예전 키로 API를 호출해 거부되는지 확인하십시오 — 이게 회전이 실제로 됐다는 증거입니다.

C 심각

결제하지 않고 주문을 완료할 수 있습니다

무슨 일이 일어나나

결제가 끝났다는 신호를 받는 주소가 있는데, 그 신호가 진짜 결제사에서 온 것인지 확인하지 않습니다. 주소만 알면 누구나 가짜 신호를 보내 돈을 내지 않고 주문을 완료시킬 수 있고, 금액도 원하는 대로 적을 수 있습니다.

누가, 어떻게 할 수 있나

필요한 것
결제 알림 주소 하나. 흔한 경로라 추측이 가능합니다
난이도
명령어 한 줄
영향 범위
무료 주문, 금액 조작, 같은 주문 반복 처리

어떻게 확인했나

코드를 읽고 발견. 결제 알림을 받는 핸들러에서 서명 검증·중복 방지·금액 재확인 세 가지를 각각 찾습니다. 시크릿 스캐너와 의존성 스캐너의 탐지 대상이 아닙니다

결제 webhook 핸들러

세 가지가 함께 빠져 있습니다. 서명 검증 없음 — 결제사에서 온 요청인지 확인하지 않습니다. 중복 방지 없음 — 같은 알림을 100번 보내면 100번 처리됩니다. 금액 재확인 없음 — 서버에서 실제 결제 금액을 다시 조회하지 않습니다.

export async function POST(req: Request) {
  const body = await req.json()
  await fulfillOrder(body.orderId, body.amount)
  return Response.json({ ok: true })
}

어떻게 고치나

아래를 Claude Code·Cursor·Codex에 그대로 붙여넣으십시오.

결제 webhook에 서명 검증, 중복 방지, 금액 재확인을 추가해줘.

작업:
1. 서명 검증: 결제사가 보낸 서명 헤더를 raw body로 검증. 실패 시 401
   (Stripe면 stripe.webhooks.constructEvent, Toss면 문서의 서명 검증 방식)
2. 멱등성: 처리한 event id를 저장하는 테이블을 만들고,
   이미 있으면 200만 반환하고 종료
3. 금액 재확인: body의 amount를 쓰지 말고 결제사 API로 해당 결제를 다시 조회해
   실제 승인 금액과 주문 금액이 일치하는지 확인. 불일치면 중단하고 로그

유지해야 할 동작: 정상 결제는 지금과 동일하게 주문이 완료돼야 한다
변경 금지: 주문 완료 후의 비즈니스 로직 (fulfillOrder 내부)

확인:
1. 서명 헤더 없이 POST → 401
2. 유효한 이벤트를 2번 전송 → 주문은 1번만 처리
3. body의 amount를 조작해 전송 → 처리 거부

고쳤는지 어떻게 확인하나

서명 헤더 없이 요청 → 401 거부. 같은 이벤트 2회 전송 → 1회만 처리. 금액을 조작해 전송 → 거부.

D 심각

커밋된 파일에 실제 키가 들어 있습니다

무슨 일이 일어나나

환경변수 파일이 값을 담은 채로 저장소에 들어가 있습니다. 저장소를 볼 수 있는 사람은 누구나 그 키로 해당 서비스를 씁니다. 저장소가 공개라면 자동 수집 프로그램이 계속 훑고 다니므로, 키가 이미 수집됐다고 보는 편이 안전합니다.

누가, 어떻게 할 수 있나

필요한 것
저장소 읽기 권한. 공개 저장소라면 아무것도 필요 없습니다
난이도
파일 열기
영향 범위
그 키가 여는 서비스 전체

어떻게 확인했나

시크릿 검사로 검출한 뒤, 파일을 하나씩 열어 실제 값인지 문서·예시·픽스처인지 가릅니다

저장소에 커밋된 환경변수 파일

공개 앱 87개에서 시크릿 검사의 원시 검출은 23곳이었습니다. README와 API 문서, 테스트 픽스처, .env.example을 걷어내니 11곳이 남았고, 그중 가장 의심스러운 4곳을 직접 열었더니 진짜 유출은 1건이었습니다. 커밋된 .env.save 하나에 JWT 비밀키와 Redis REST 토큰, 데이터베이스 접속 문자열이 함께 들어 있었습니다. 나머지 7곳은 열어보지 않았습니다. 같은 4곳에서 나온 오탐이 더 중요합니다. 릴리스 스크립트에서 “Supabase 키 유출”로 보고된 값을 열어 JWT를 디코드하니 role이 anon이었습니다. 브라우저에 실려 나가라고 만든 공개 키입니다. 그대로 리포트에 옮겼다면 고객은 멀쩡한 키를 회전시키느라 앱을 멈췄을 겁니다.

# .env.save — 저장소에 커밋돼 있습니다
JWT_SECRET=eyJhbGciOiJIUzI1NiIs...
UPSTASH_REDIS_REST_TOKEN=...
DATABASE_URL=postgresql://postgres:...@db...supabase.co:5432/postgres

어떻게 고치나

회전 순서는 위 “지금 10분 안에”와 같습니다. 파일을 지우는 것만으로는 아무것도 해결되지 않습니다 — 이력에 그대로 남고, 이미 나간 키는 계속 유효합니다.

아래를 Claude Code·Cursor·Codex에 그대로 붙여넣으십시오.

커밋된 파일에 실제 키가 들어 있어. 회전하고 이력에서 지워줘.

작업:
1. 실제 값이 담긴 파일(.env.save 등)을 저장소에서 제거하고 .gitignore에 추가
2. 각 서비스 대시보드에서 노출된 키를 전부 회전
3. 새 키는 배포 플랫폼의 환경변수에만 넣는다. 저장소에 다시 커밋하지 않는다
4. 과거 커밋 이력에서 제거 (git filter-repo 또는 BFG). 이력 재작성이므로
   협업자에게 먼저 알리고 강제 푸시 시점을 합의한다
5. 예시가 필요하면 .env.example에 자리표시자만 남긴다

유지해야 할 동작: 배포된 앱이 새 키로 계속 동작해야 한다
변경 금지: 애플리케이션 로직

확인: 저장소 전체에서 노출됐던 키 문자열 검색 → 결과 없음. 예전 키로 API 호출 → 거부

고쳤는지 어떻게 확인하나

저장소 전체에서 노출됐던 키 문자열을 검색해 결과가 없는지 확인하십시오. 그리고 예전 키로 각 서비스 API를 호출해 전부 거부되는지 확인하십시오. 파일만 지우고 회전을 건너뛰면 아무것도 바뀌지 않습니다.

이번 주

위 항목들을 처리한 뒤 이어서 고치십시오.

E 높음

AI 호출에 출력 상한이 없습니다

무슨 일이 일어나나

AI 기능을 쓸 때마다 요금이 나가는데, 응답 길이에도 사용자별 호출 수에도 상한이 없습니다. 반복 호출을 자동화하면 요금이 계속 올라갑니다. 보안 사고는 아니지만 금전 손실은 같습니다.

누가, 어떻게 할 수 있나

필요한 것
로그인. 인증 확인이 없으면 로그인 없이도 가능합니다
난이도
반복 요청 스크립트
영향 범위
AI 사용료. gpt-4 출력 단가는 100만 토큰당 $60이고 상한을 4,000토큰으로 두면 요청 1건당 최대 $0.24입니다. 분당 60회 자동 호출이면 시간당 최대 $864입니다. 계정 등급별 분당 토큰 제한에 걸리면 실제 상한은 이보다 낮아집니다 — 대시보드에서 현재 등급을 확인하십시오

어떻게 확인했나

코드를 읽고 발견. LLM 호출 지점에 출력 상한이 있는지, 그리고 상한을 거는 코드가 다른 파일에 있는지까지 봅니다. 시크릿 스캐너와 의존성 스캐너의 탐지 대상이 아닙니다

LLM 호출 지점

사용자별 호출 제한, 일일 상한, 예산 알림이 모두 없습니다. 인증 확인도 없습니다. 출력 상한을 다른 파일에서 걸어둔 경우가 있으므로, 이 항목은 호출 지점만 보고 판정하지 않고 상한을 거는 코드가 어디에도 없는지 확인한 뒤 올립니다.

어떻게 고치나

예산 상한 설정은 위 “지금 10분 안에”에 있습니다. 코드와 별개로 그게 마지막 방어선입니다.

아래를 Claude Code·Cursor·Codex에 그대로 붙여넣으십시오.

LLM을 호출하는 API route에 사용량 제한을 추가해줘.

작업:
1. 사용자별 rate limit: 분당 5회, 일일 50회 (사용자id + 날짜로 카운트)
2. 초과 시 429와 안내 메시지
3. 출력 상한(max_tokens)을 1000으로 명시
4. 인증 안 된 요청은 401 (현재 누구나 호출 가능)
5. 일일 전체 호출이 임계치를 넘으면 기능을 자동 중단하는 kill switch

유지해야 할 동작: 정상 사용자의 일반적인 사용은 막히지 않아야 한다
변경 금지: 프롬프트 내용, 모델 선택 로직

확인: 같은 사용자로 분당 6회 호출 → 6번째가 429. 인증 없이 호출 → 401

고쳤는지 어떻게 확인하나

같은 사용자로 분당 6회 호출 → 6번째가 429. 인증 없이 호출 → 401. 그리고 응답이 상한에서 잘리는 경우가 얼마나 되는지 로그로 확인하십시오 — 자주 잘리면 상한이 낮은 것이고, 그건 따로 조정할 문제입니다.

F 높음

알려진 취약점이 있는 의존성이 쌓여 있습니다

무슨 일이 일어나나

설치된 라이브러리 중에 공개된 취약점이 있는 버전이 남아 있습니다. 공개된 취약점은 공격 코드도 함께 도는 경우가 많아, 버전만 보고 시도해 볼 수 있습니다. 대부분 버전을 올리면 끝납니다.

누가, 어떻게 할 수 있나

필요한 것
lockfile. 공개 저장소라면 누구나 읽습니다
난이도
공개된 취약점 목록과 대조
영향 범위
패키지에 따라 다릅니다. 인증·요청 처리 계열이면 우회로 이어집니다

어떻게 확인했나

의존성 검사

lockfile

공개 앱 87개 중 83곳에서 lockfile을 측정했습니다. 취약점이 있는 의존성은 저장소당 중앙값 81건, 최대 253건이었습니다. CVSS 9.0 이상을 하나라도 가진 곳이 83곳 중 46곳입니다. 이 항목을 위 넷과 같은 무게로 세지 않는 이유는, 대부분 업데이트로 해결되고 판단이 필요 없기 때문입니다.

어떻게 고치나

아래를 Claude Code·Cursor·Codex에 그대로 붙여넣으십시오.

알려진 취약점이 있는 의존성을 올려줘.

작업:
1. 락파일 기준으로 알려진 취약점 목록을 만든다
2. CVSS 9.0 이상부터 올린다. 메이저 업그레이드가 필요한 것은 따로 표시
3. 한 번에 하나씩 올리고, 빌드와 기존 동작을 확인한 뒤 다음으로 넘어간다
4. 상위 패키지에 묶여 올릴 수 없는 것은 이유를 적어 남긴다

유지해야 할 동작: 빌드가 통과하고 기존 화면·API가 동일하게 동작해야 한다
변경 금지: 애플리케이션 로직

확인: 같은 검사를 다시 돌려 CVSS 9.0 이상 0건

고쳤는지 어떻게 확인하나

같은 검사를 다시 돌려 CVSS 9.0 이상이 0건인지 확인하십시오. 남은 것이 있으면 왜 못 올렸는지 한 줄로 적어두십시오 — 다음 검사에서 같은 항목을 다시 판단하지 않아도 됩니다.

지금 그대로 두면 새는 돈

위 항목들과 달리 출시를 막지 않습니다. 대신 매달 나갑니다. 기능을 바꾸지 않고 줄일 수 있는 것만 적었습니다.

무엇지금 바꾸면차이
AI 모델
LLM 호출 지점
gpt-4
출력 100만 토큰당 $60
gpt-4o
출력 100만 토큰당 $10
6분의 1
요청당 $0.24 → $0.04
응답 길이
같은 호출
상한 없음 max_tokens 1000
채팅 응답에 4000토큰은 거의 쓰이지 않습니다
4분의 1
4,000토큰 기준

둘을 함께 바꾸면 AI 요청 1건당 $0.24가 $0.01 아래로 내려갑니다. 모델과 응답 길이를 같이 줄이기 때문입니다. 위 E(호출 상한)와는 다른 문제입니다. E는 폭주를 막는 것이고 이건 평상시 단가를 낮추는 것입니다. 둘 다 해야 합니다.

앱 안에서 줄일 수 없는 비용

전송량 과금은 위 두 줄과 다릅니다. 코드에서 단가를 낮추는 일이 아니라 대시보드에서 상한을 켜는 일이고, 그 항목은 위 “지금 10분 안에” 3번에 있습니다. 이 리포트는 실제 청구 내역과 전송량 추이를 보지 못했습니다. 대시보드 읽기 권한이 있어야 확인됩니다.

바꾸기 전에 확인하실 것

모델을 바꾸면 응답이 달라질 수 있습니다. 쓰시는 프롬프트 몇 개를 두 모델에 각각 넣어보고 결과가 쓸 만한지 직접 보신 뒤에 바꾸십시오. 비용이 6분의 1이어도 답이 못 쓰겠으면 절감이 아닙니다. 단가는 2026-08-15에 OpenAI 공개 가격표에서 확인한 값이고, 위 금액은 그 가격표로 계산한 값입니다. 가격은 바뀌므로 적용 시점에 다시 보십시오.

어떻게 고치나

아래를 Claude Code·Cursor·Codex에 그대로 붙여넣으십시오.

LLM 호출의 단가를 낮춰줘.

작업:
1. model을 'gpt-4'에서 'gpt-4o'로 교체
2. max_tokens를 1000으로 명시 (현재 상한 없음)
3. 응답이 잘리는 경우를 대비해 finish_reason이 'length'면
   로그를 남기도록 추가 (상한이 실제로 부족한지 확인하기 위해)

유지해야 할 동작: 요청·응답 형식과 화면에 보이는 결과 구조는 그대로
변경 금지: 프롬프트 내용

확인: 같은 프롬프트 5개를 바꾸기 전후로 호출해 응답을 나란히 비교.
      쓸 만하면 유지, 아니면 gpt-4o 유지한 채 max_tokens만 올려서 재비교

이미 악용됐는지 확인하는 법

운영 중인 앱이라면 고치는 것만으로 끝나지 않습니다. 이미 일어난 일이 있는지 확인해야 합니다. 아래는 대시보드에서 직접 볼 수 있는 것들입니다.

무엇을어디서 무엇을 찾나
규칙 없는 테이블의 대량 조회Supabase → Logs → API같은 테이블을 짧은 시간에 반복 조회한 요청, 평소보다 큰 응답 크기
마스터 키 오용Supabase → Logs → APIservice_role로 들어온 요청 중 서버 IP가 아닌 것
결제 없는 주문결제사 대시보드 ↔ 주문 테이블기간을 정해 승인 건수와 완료 주문 수를 대조. 주문이 더 많으면 그 차이가 의심 건
중복 처리주문 테이블같은 결제 id로 완료된 주문이 둘 이상인 경우
AI 요금 급증AI 제공사 → Usage일자별 급등 구간, 그 시각의 서버 로그
전송량 급증호스팅 대시보드 → Usage짧은 시간에 몰린 대역폭, 같은 경로로 반복된 요청

흔적이 없다고 해서 없었다고 단정할 수는 없습니다. 로그 보관 기간이 짧으면 그 이전은 확인이 불가능합니다. 확인한 기간을 함께 기록해 두십시오.

발견들이 어떻게 얽히나

각각을 따로 보면 우선순위를 잘못 잡습니다. 이 앱에서는 세 가지 관계가 중요합니다.

B를 두면 A를 고쳐도 소용이 없습니다

A(접근 규칙)는 데이터베이스가 요청자를 보고 거르는 방어입니다. B의 마스터 키는 그 규칙을 통째로 무시합니다. A만 고치고 B를 두면 방어가 성립하지 않습니다. B의 키 회전이 먼저입니다.

B와 D는 같은 회전 한 번으로 끝나지 않습니다

B는 번들에 실린 키고 D는 저장소에 커밋된 키입니다. 나간 경로가 다르므로 어느 쪽을 고쳐도 다른 쪽의 키는 계속 유효합니다. 회전은 한 번에 하되, 노출된 키 목록을 먼저 두 곳에서 모아야 합니다.

A를 정책 없이 켜면 앱이 그 자리에서 멈춥니다

규칙만 켜고 정책을 넣지 않으면 모든 조회가 빈 결과가 됩니다. 사용자에게는 데이터가 사라진 것처럼 보입니다. A의 수정 지시가 둘을 같은 마이그레이션 파일에 넣으라고 적은 이유가 이것입니다.

한 줄만 고치면 무엇이 남나

위 수정 지시가 왜 한 줄이 아닌지에 대한 설명입니다. 각 항목에서 가장 눈에 띄는 수정 하나만 했을 때 무엇이 그대로 남는지 적었습니다. 코딩 에이전트로 직접 고치실 때 이 표를 옆에 두십시오.

눈에 띄는 수정그래도 남는 것
A 규칙을 켠다 정책 없이 켜면 앱이 멈춥니다. 정책을 넣어도 anon 역할에 남은 GRANT는 그대로입니다
B 키를 서버 전용으로 옮긴다 회전을 안 하면 이미 나간 키가 그대로 유효합니다. 코드는 깨끗한데 데이터베이스는 계속 열려 있습니다
C 서명 검증을 넣는다 같은 알림을 여러 번 보내면 여러 번 처리되고, 금액은 여전히 요청이 말한 값을 믿습니다
D 파일을 지운다 이력에 그대로 남고, 회전하지 않은 키는 계속 유효합니다. 셋을 다 해야 끝납니다
E 호출 제한을 건다 인증 확인이 없으면 로그인 없이 호출하는 경로는 그대로 열려 있습니다
F 최신 버전으로 한 번에 올린다 메이저 업그레이드가 섞이면 무엇이 깨졌는지 알 수 없습니다. 하나씩 올려야 되돌릴 수 있습니다

고객의 에이전트가 실제로 무엇을 빠뜨리는지는 케이스마다 기록하고 있습니다. 숫자가 쌓이면 분모와 함께 공개합니다.

유출이 확인되면 해야 하는 것

A에서 규칙이 빠진 테이블에 연락처나 주문이 들어 있으면 개인정보에 해당합니다. 실제 열람 정황이 확인되면 설명은 선택이 아니라 법정 의무가 됩니다. 아래는 개인정보 보호법 제34조와 시행령 제40조의 요건입니다.

무엇을언제까지 어디에
정보주체 통지 지체 없이 영향받은 이용자 본인
신고 72시간 이내 개인정보보호위원회 또는 한국인터넷진흥원(KISA) — 접수 창구는 개인정보 포털

신고 대상 기준은 정보주체 1천명 이상 유출, 또는 민감정보·고유식별정보 유출입니다. 미신고 시 같은 법 제75조에 따라 3천만원 이하의 과태료가 부과될 수 있습니다. 기준선 근처라면 판단을 미루지 말고 창구에 먼저 문의하십시오.

누가 물으면

고객이 물으면

“외부 점검에서 접근 권한 설정 문제를 발견해 즉시 수정했습니다. 확인 가능한 기간의 기록을 검토했고 현재까지 확인된 열람 정황은 없습니다. 추가로 확인되는 내용이 있으면 바로 알리겠습니다.”

로그를 실제로 확인한 뒤에만 쓰십시오. 정황이 나왔다면 이 문장 대신 위 표의 통지·신고 절차로 갑니다.

투자자·파트너가 물으면

“출시 전 외부 점검을 받았고, 심각 항목을 수정했습니다. 점검 범위와 확인하지 못한 범위가 함께 적힌 리포트를 드릴 수 있습니다.”

“재검사로 확인했습니다”는 재검사를 실제로 받은 경우에만 쓸 수 있습니다. 이 리포트는 무료 점검이라 재검사가 포함돼 있지 않습니다.

말을 고를 때

“현재까지 확인된 열람 정황은 없습니다”와 “유출은 없었습니다”는 다른 말입니다. 앞의 문장만 쓰십시오. 뒤의 문장은 로그가 남지 않은 기간까지 보증하는 말이 됩니다.

확인하지 못한 범위

출시를 막는 것은 보안만이 아닙니다. 아래는 레인별로 이 리포트가 무엇을 봤고 무엇을 못 봤는지입니다. 못 본 칸에 문제가 없다는 뜻이 아닙니다. 볼 수 없었다는 뜻입니다.

레인이 리포트가 본 것 못 본 것 — 보려면 필요한 것유료
보안·데이터 접근 규칙, 시크릿, 결제 검증 — A~D 배포 환경의 실제 권한 동작(스테이징 URL + 테스트 계정), Supabase·결제사 실제 설정(읽기 전용 커넥터), 과거 커밋 이력
비용 AI 호출 상한 — E 실제 청구 내역과 사용 추이, 전송량 과금과 지출 상한이 실제로 켜져 있는지 (대시보드 읽기 권한)
인프라·운영 의존성 — F. 그리고 저장소에 들어 있는 컨테이너·서버 설정 — Dockerfile의 마지막 USER, compose의 포트 지도, 설정 모듈의 디버그·허용 호스트, nginx 설정 돌고 있는 인프라 — 배포 설정, 환경변수, 백업, 롤백, 관측성. 저장소의 Dockerfile과 compose가 실제 배포 구성이라는 보장도 없습니다. 클라우드 접근 권한이 없습니다 전문가 검토만
코드·로직 입력·검증 경로 — C 테스트 유무와 커버리지, 오류 경로 처리, 중복·죽은 코드, 상태 흐름 전수
설계·사용성 없음 — 이번 검사 범위 밖입니다 모바일 레이아웃, 키보드 접근, 핵심 여정의 실제 화면 — 앱을 실행해야 알 수 있습니다

보안·비용 두 레인이 두껍습니다. 인프라·운영은 저장소에 적혀 있는 데까지만이고, 나머지 둘은 얇습니다. 그게 지금 이 상품의 모양입니다. 얇은 칸을 두껍게 만들려면 위에 적은 접근 권한이 필요합니다.

범위 밖에서 더해지는 것도 있습니다. 점검 전 문답으로 무엇이 중요한 기능인지 먼저 정하고, 수정 후 재검사를 출시 점검은 1회, 전문가 검토는 2회 합니다. 전문가 검토에는 외부에 보여줄 수 있는 검증 문서가 포함됩니다. 출시 후 이 판정을 계속 유지하는 관제는 별도 구독입니다.

이 리포트가 보증하지 않는 것

  • 취약점이 더 없다는 뜻이 아닙니다. 위 범위는 검사되지 않았습니다.
  • 안전 인증이 아닙니다. 특정 시점, 특정 범위의 관찰 결과입니다.
  • 이미 악용된 적이 없다는 뜻이 아닙니다. 확인 가능한 로그 범위만 봤습니다.
  • 자동 검사는 배포된 룰이 아는 패턴만 찾습니다. 이 앱만의 논리 허점은 코드를 읽어야 나옵니다.
  • 법률 자문이 아닙니다. 위 통지·신고 요건은 조문 그대로이며, 적용 여부 판단은 창구나 대리인과 확인하십시오.
  • 수정 후에는 반드시 재검사하십시오. 수정이 다른 것을 깨뜨릴 수 있습니다.

같은 리포트를 받아보세요.

저장소 주소나 URL을 보내주시면 하루 안에 정리해서 드립니다. 무료입니다.