보안 배경이 없는 인디 개발자가 흔히 겪는 사례를, 식별 가능한 세부 정보를 모두 제거하고 교훈으로 바꾼 글입니다. 여기에 공격 재현 절차는 없습니다. 목표는 방어이며, 같은 실수가 당신에게 일어나지 않게 하는 것입니다.
- 분류
- API 키 유출 / 부정 과금
- 심각도
- 크리티컬 (공개된 CVSS 10.0이 악용됨)
- 근본 원인
- 웹 프레임워크의 인증 전(pre-auth) RCE가 몇 달간 패치되지 않은 채 방치됨
- 유출 범위
- 환경의 모든 시크릿
- 실행 범위
- 권한 없는 컨테이너 내부에 머무름(호스트 root는 무사)
- 영구 조치
- ①패치 버전으로 업그레이드 ②모든 키 교체 ③기계 CVE 모니터링
“과금을 멈췄다” ≠ “처리했다”
청구를 멈추는 것은 임시 조치다. 유출 경로를 닫는 것은 별개의 작업이다. 둘 다 끝냈을 때 비로소 완료된 것이다.
무슨 일이 있었나 (타임라인)
0일째 — 출시·가동
AI의 도움으로 만든 앱이 출시되어 프로덕션에서 가동 중이었다.어느 날 — 요금 급증
클라우드 AI 요금이 갑자기 치솟는다. 결코 쓸 일 없는 모델에서 대량 작업이, 자기 것이 아닌 일을 하고 있었다.조사 — 제3자의 악용
"우리 앱이 폭주했다"로는 설명되지 않는다. 제3자가 탈취한 키로 그것을 돌리고 있었다.추적 — 유출 경로 조사
"그럼 키는 어디서 유출됐나?" 실제 조사는 여기서 시작됐다.
처음에 잡히지 않은 이유
실수가 저질러진 순서대로 — 실수가 곧 교훈이기 때문이다.
원인을 너무 일찍 단정
→ "내 잘못"이나 "남의 잘못"을 정하기 전에, 먼저 데이터를 보라.
깨끗한 grep 결과로 조사를 끝낼 뻔했다
증상을 가리는 것을 고친 것으로 착각
깨끗한 grep은 안전 확인이 아니다
어떤 파일에도 키가 없더라도, 취약점은 실행 중인 프로세스에서 환경 변수를 빼낼 수 있다. 유출은 파일뿐 아니라 런타임(RCE, HTTP 헤더)에서도 일어난다. RCE란 무엇인가 참고.
실제 원인: 패치되지 않고 방치된 공개 CVSS 10.0
오래된 액세스 로그의 이상한 시그니처가 위협 인텔리전스와 곧바로 일치했다. 사용 중이던 웹 프레임워크 버전 범위에는 공개된 인증 전 RCE(CVSS 10.0) 가 있었고, 이미 실제로 악용되고 있었으며 — 몇 달간 패치되지 않은 채 가동되고 있었다.
- 이것은 수동적인 버그가 아니라, 공격자가 서버에서 코드를 실행해 환경을 빼낸 것이었다.
- 피해를 제한한 점이 하나 있었다: 실행은 권한 없는 컨테이너 내부에 머물렀다(호스트 root는 탈취되지 않았다). 침해 검토 결과 지속형 백도어, 마이너, C2는 없었고 — 확인된 피해는 시크릿 탈취였다.
- 내부 DB에 도달 가능했기 때문에, 대응은 DB 내용도 유출됐다고 가정했다.
교훈: 무언가 이상하게 동작하면, 먼저 알려진 CVE를 의심하라. 자기 버그라고 단정하지 마라.
유출 경로 좁히기: 그 키는 어디에 있었나?
grep으로 아무것도 나오지 않으면 찾는 방법을 바꿔라. 악용된 키가 적혀 있던 곳을 나열하면 유출 경로 후보가 줄어든다. 예를 들어 악용된 키가 운영 서버의 환경 변수에만 있었다면, Git이나 브라우저가 아니라 운영 서버나 호스팅 업체를 의심해야 한다.
| 유출되는 곳 | 흔한 원인 | 내 설정을 확인하는 법 | 막는 법 |
|---|---|---|---|
| Git 히스토리 / 공개 저장소 | .env 커밋, 키 하드코딩 | gitleaks git으로 전체 히스토리를 스캔. GitHub 시크릿 스캐닝 알림 확인 | 먼저 폐기하고, 그다음 커밋 전에 막는다 |
| 브라우저로 전송되는 JavaScript | NEXT_PUBLIC_ 변수에 키를 넣음, 브라우저에서 API를 직접 호출 | 빌드 결과물에서 키의 접두어를 검색 | API 호출을 서버 측 코드로 옮긴다 |
| 공개 웹 폴더 | .env나 백업을 공개 폴더에 둠 | 내 사이트의 /.env를 열어 404나 403이 나오는지 확인 | 시크릿 파일을 공개 폴더 밖으로 옮긴다 |
| 로그 / 오류 페이지 | 디버그 출력을 켠 채 출시, 요청 헤더를 그대로 기록 | 운영 환경의 디버그 설정과 로그에 Authorization 등이 남는지 확인 | 디버그 출력을 끄고 로그의 시크릿을 가린다 |
| 가동 중인 서버 | 공개된 취약점(RCE 등)을 패치하지 않고 방치 | 실제 가동 중인 버전과 권고문의 수정 버전을 비교 | 업그레이드한 뒤 환경의 모든 시크릿을 교체한다 |
| 호스팅 업체 | 업체 측 침해 | 업체의 보안 공지와 이메일 안내 확인 | 저장 후 값을 다시 볼 수 없게 시크릿을 저장하고 교체한다 |
| 개발용 컴퓨터 | 악성 패키지나 확장 기능 | 설치한 패키지·확장 기능 버전이 공개된 침해 사례와 일치하는지 확인 | 해당 버전을 제거하고 그 컴퓨터에 있던 시크릿을 교체한다 |
이번 사례는 "가동 중인 서버" 행이었다. 아래 네 행은 코드나 Git에 흔적이 남지 않는 경로다. OWASP의 Secrets Management Cheat Sheet도 환경 변수는 같은 환경의 다른 프로세스가 읽을 수 있는 경우가 많고 로그나 덤프에 남을 수 있다고 경고한다. 그러니 키 하나가 악용되었다면, 같은 곳에 저장된 모든 시크릿을 유출된 것으로 다뤄라.
키를 나눠 두면 조사가 빨라진다
서비스마다, 환경마다(운영과 개발) 다른 키를 쓰면, 악용된 키만 보고도 표의 어느 행인지 알 수 있다. 한 키를 어디에나 재사용하면 모든 장소를 의심해야 한다. 키를 나누는 법은 탈취된 API 키로 OpenAI 계정이 정지될 때에서 다룬다.
취약점은 실제 가동 중인 버전으로 세라
다른 의존성을 확인하자 "취약한 의존성이 8개 더 있다"고 보고됐다. 이것도 틀렸다. package.json의 하한값(^ 캐럿 범위)으로 센 것이었다. 진짜 위험한 것은 남아 있던 고정(pinned) 의존성 2개뿐이었다.
자신의 가동 중인 버전을 확인하라
락파일이나 가동 중인 컨테이너에서, 실제로 해석된 버전을 보라.
# npm: see what's actually installed
npm ls next react react-dom
# inside a running container
docker exec <container> npm ls <package>여기 나오는 숫자가 실제로 가동 중인 버전이다. package.json의 ^가 아니다.
내 설정을 점검하는 법
이상한 청구서를 막 발견했든 아직 아무 일도 없든, 다음 순서로 확인할 수 있다. 모든 단계는 내 계정과 서버만 본다.
어느 키가 언제 쓰였는지 본다
브라우저로 전송되는 파일을 확인
NEXT_PUBLIC_으로 시작하는 환경 변수의 값이 빌드 시 브라우저로 보내는 JavaScript에 그대로 삽입된다. 빌드한 뒤 결과물에서 키의 접두어를 검색하라. 예: grep -rl "sk-" .next/static. 여기서 발견된 것은 누구나 읽을 수 있었던 것이다.시크릿 파일이 공개 폴더에서 접근되지 않는지 확인
curl -sI https://example.com/.env를 실행하고(example.com을 내 도메인으로 바꿔서), 첫 줄의 상태가 200이 아닌지(404나 403이어야 한다) 확인하라. 자세한 내용은 공개 디렉터리의 시크릿 파일 점검에 있다.운영 버전을 권고문과 비교
npm ls를 실행해 버전을 확인하라. osv-scanner scan -L pnpm-lock.yaml은 락파일에 알려진 취약점이 있는 버전이 포함되어 있는지 확인한다.로그에 시크릿이 있는지 확인
gitleaks dir --redact를 실행해 로그에 기록된 키를 찾아라. --redact는 발견된 값 자체가 화면에 나오지 않게 한다.한도와 알림 확인
가장 먼저 한 일 (재현 가능한 절차)
악용된 키를 즉시 폐기
프레임워크를 패치 버전으로 업그레이드
모든 시크릿 교체
침해 검토
계정 측 방어
예방: CVE를 자동으로 모니터링하라
요약하면, 이 사고는 "사람이 공개된 CVSS 10.0을 놓쳤다"였다. 수동 확인은 늘 무언가를 놓친다. 자동 모니터링은 그것을 잡아낸다.
오늘 시작할 수 있는 의존성 스캔
무료로 시작 — CI에 한 단계.
# OSV scanner (Google) checks your lockfile
osv-scanner scan -L pnpm-lock.yaml
# On GitHub, enable Dependabot (repo Settings → Security)본 사이트 역시 이 교훈에 따라 자신의 의존성을 CVE 모니터링한다 — 권하는 것을 직접 실천한다.
흔한 실수와 고치는 법
- 아직 열려 있는 서버에 새 키를 넣는다: 같은 경로로 새 키도 탈취된다. 악용된 키는 바로 멈추고, 취약점을 고친 다음 나머지 시크릿 교체를 마무리하라.
- 예산 알림을 상한으로 여긴다: Google Cloud는 알림만 있는 예산은 사용량이나 지출을 자동으로 멈추지 않는다고 밝힌다. 멈추게 하고 싶다면 OpenAI의 월간 하드 한도나 Anthropic의 Workspace 지출 한도처럼 사용을 차단하는 한도를 더하라.
- AI API 키를 브라우저나 모바일 앱에 넣는다: 사용자 기기에 도달하는 값은 무엇이든 빼낼 수 있다. AI API는 내 서버에서 호출하고, 브라우저는 내 서버를 호출하게 하라.
- 파일이나 Git 히스토리에서 지우고 거기서 멈춘다: 이미 복사한 사람은 여전히 쓸 수 있다. 먼저 폐기하고, 히스토리 정리는 나중에 하라(유출된 GitHub 인증 정보가 계속 유효한 이유 참고).
- 개발과 운영에서 같은 키를 쓴다: 어느 쪽에서 유출됐는지 알 수 없어 둘 다 멈춰야 한다. 환경마다 별도의 키를 발급하라.
공개 사례: 업체 측에서 유출된 환경 변수
어떤 파일 검색으로도 찾을 수 없는 유출은 내 서버 밖에서도 일어난다. 2026년 4월 19일, Vercel은 내부 시스템에 대한 무단 접근을 공개했다. 회사에 따르면, 직원이 사용하던 제3자 AI 도구가 침해된 것을 계기로, 일부 고객의 환경 변수 중 "sensitive"(저장 후 값을 볼 수 없게 하는 설정)로 지정되지 않은 것이 탈취되었다.
Vercel은 고객에게 sensitive로 지정되지 않은 환경 변수를 검토·교체하고, 활동 로그와 최근 배포를 확인하라고 권고했다. 또한 프로젝트나 계정을 삭제하는 것만으로는 위험이 사라지지 않는다고 밝혔다.
그런 상황의 고객은 코드와 Git을 아무리 검색해도 유출 흔적을 찾지 못했을 것이다. 해야 할 일은 이번 사례와 같다. 그 장소에 저장된 모든 시크릿을 교체하는 것이다. 절차는 Vercel 보안 사고(2026년 4월)에 있다.
교훈
흔한 실수
- "과금을 멈췄다"에서 완료로 친다
- grep이 깨끗하다고 안심한다
- 프록시에서 증상을 가리고 고쳤다고 느낀다
- 취약점을
package.json하한으로 센다 - 악용이 확인된 키 하나만 교체한다
올바른 방어
- 과금 중지와 유출 경로 차단을 별개 작업으로 보고, 둘 다 한다
- 런타임 유출(RCE, 헤더)도 의심한다
- 원인이 된 프레임워크를 갱신한다
- 실제로 가동 중인 버전으로 판단한다
- 환경이 유출되면 환경 전체를 교체한다
정리하면: 요금 급증은 증상이었고, 원인은 패치되지 않고 방치된 CVSS 10.0 RCE였다. 올바른 원인은 첫 시도에 찾은 것이 아니라, 여러 번 틀리고 다시 확인하는 과정에서 찾았다.
다음으로 읽기
- 유출 후: GitHub에 노출된 인증 정보 543,699건이 아직 유효하다 — 삭제가 아니라 폐기해야 하는 이유
- 용어집: RCE란 무엇인가 · CVE란 무엇인가 · CVSS란 무엇인가
- 방어: Next.js를 안전하게 운영하기(CVE 위생)
- 사고: .env가 온 세상에 노출됐을 때
- 기본: .env와 API 키의 기본(유출 후 할 일)
- 예방: gitleaks로 커밋 전에 시크릿을 막기
- 사고: 탈취된 API 키로 OpenAI 계정이 정지될 때
출처
- OWASP, "Secrets Management Cheat Sheet"(환경 변수의 위험, 유출 시 대응): cheatsheetseries.owasp.org
- Next.js Docs, "How to use environment variables in Next.js"(
NEXT_PUBLIC_의 인라인 삽입): nextjs.org - OSV-Scanner, "Usage": google.github.io
- OpenAI, "Production best practices"(키 관리, 알림과 한도): developers.openai.com
- Claude Help Center, "Creating and managing Workspaces"(Workspace 한도와 키): support.claude.com
- Google Cloud, "Create, edit, or delete budgets and budget alerts": docs.cloud.google.com
- Vercel, "Vercel April 2026 security incident": vercel.com