오늘 안에
코드를 고쳐야 하는 항목입니다. 지금 이 순간 누구나 실행할 수 있으니 하루를 넘기지 마십시오.
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만 올려서 재비교