보안에 익숙하지 않은 인디 개발자가 흔히 겪는 사례를, 식별 가능한 세부 정보를 모두 제거하고 교훈으로 바꾼 글입니다. 여기에 공격 재현은 없습니다 — 목표는 방어이며, 같은 실수가 당신에게 일어나지 않게 하는 것입니다.
- 분류
- 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를 의심하라. 자기 버그라고 단정하지 마라.
개수도 틀렸다 — 가동 중인 버전으로 판단하라
조사가 번지며 "취약한 의존성이 8개 더 있다"고 보고됐다 — 이것도 틀렸다. package.json의 하한값(^ 캐럿 범위)으로 센 것이었다. 진짜 위험한 것은 남아 있던 고정(pinned) 의존성 2개뿐이었다.
자신의 가동 중인 버전을 확인하라
락파일이나 가동 중인 컨테이너에서, 실제로 해석된 버전을 보라.
# npm: 실제로 설치된 것을 확인
npm ls next react react-dom
# 가동 중인 컨테이너 안에서
docker exec <container> npm ls <package>여기 나오는 숫자가 진실이다 — package.json의 ^가 아니라.
가장 먼저 한 일 (재현 가능한 절차)
악용된 키를 즉시 폐기
프레임워크를 패치 버전으로 업그레이드
모든 시크릿 교체
침해 검토
계정 측 방어
진짜 예방책: 기계가 감시하게 하라
요약하면, 이 사고는 "사람이 공개된 CVSS 10.0을 놓쳤다"였다. 수동 순찰은 늘 무언가를 놓친다. 기계는 그렇지 않다.
오늘 시작할 수 있는 의존성 스캔
무료로 시작 — CI에 한 단계.
# OSV 스캐너(Google)가 락파일을 점검한다
osv-scanner --lockfile=pnpm-lock.yaml
# GitHub에서는 Dependabot을 활성화(저장소 Settings → Security)본 사이트 역시 이 교훈에 따라 자신의 의존성을 CVE 모니터링한다 — 권하는 것을 직접 실천한다.
교훈
흔한 실수
- "과금을 멈췄다"에서 완료로 친다
- grep이 깨끗하다고 안심한다
- 프록시에서 증상을 가리고 고쳤다고 느낀다
- 취약점을
package.json하한으로 센다 - 악용이 확인된 키 하나만 교체한다
올바른 방어
- 응급 처치와 "유출 경로 차단"을 별개 작업으로 보고, 둘 다 한다
- 런타임 유출(RCE, 헤더)도 의심한다
- 근본(프레임워크)을 갱신한다
- 실제로 가동 중인 버전으로 판단한다
- 환경이 유출되면 환경 전체를 교체한다
한 줄로: 폭주한 청구서는 빙산의 일각이었다. 진짜 원인은 방치된 CVSS 10.0 RCE였고 — 진실은 운 좋은 한 번의 추측이 아니라, 틀리고, 부딪치고, 오류를 깎아내는 과정에서 나왔다.
다음으로 읽기
- 용어집: RCE란 무엇인가 · CVE란 무엇인가 · CVSS란 무엇인가
- 방어: Next.js를 안전하게 운영하기(CVE 위생)
- 사고: .env가 온 세상에 노출됐을 때
FAQ
QAPI 키가 유출됐다면, 악용된 그 하나의 키만 폐기하면 충분한가요?
아니요. 유출 경로가 런타임(RCE나 헤더 유출)에 있다면 환경의 모든 시크릿이 한꺼번에 유출됐다고 가정하고 전부 교체하세요. 악용된 것을 본 키는 빙산의 일각일 뿐입니다.
Q코드와 git 전체를 grep 해도 키가 나오지 않으면 안전한가요?
반드시 그렇지는 않습니다. 어떤 파일에도 키가 없더라도, 프레임워크 취약점이 실행 중인 프로세스에서 환경 변수를 직접 빼낼 수 있습니다. 유출은 파일뿐 아니라 런타임에서도 일어납니다.
Q이 일이 다시 일어나지 않게 하는 가장 확실한 방법은 무엇인가요?
의존성을 기계로 CVE 모니터링하는 것입니다. 여기서의 근본 원인은 사람이 공개된 CVSS 10.0을 몇 달간 놓친 것이었습니다. Dependabot이나 osv-scanner가 그 빈틈을 구조적으로 메웁니다.