보안 가이드

AI가 작성한 코드에서 API 키가 유출돼 부정 과금에 쓰였다: 원인은 패치되지 않은 CVSS 10.0 취약점

AI로 만든 앱에서 API 키가 탈취돼 부정 과금이 쌓인 익명 사례입니다. 원인을 잘못 짚은 과정, 패치되지 않은 CVSS 10.0 RCE를 찾은 방법, 재발 방지책을 다룹니다.

게시 2026-06-07 업데이트 2026-10-06 11분 읽기
이 페이지에서

보안 배경이 없는 인디 개발자가 흔히 겪는 사례를, 식별 가능한 세부 정보를 모두 제거하고 교훈으로 바꾼 글입니다. 여기에 공격 재현 절차는 없습니다. 목표는 방어이며, 같은 실수가 당신에게 일어나지 않게 하는 것입니다.

사례 파일 — INCIDENT FILE
분류
API 키 유출 / 부정 과금
심각도
크리티컬 (공개된 CVSS 10.0이 악용됨)
근본 원인
웹 프레임워크의 인증 전(pre-auth) RCE가 몇 달간 패치되지 않은 채 방치됨
유출 범위
환경의 모든 시크릿
실행 범위
권한 없는 컨테이너 내부에 머무름(호스트 root는 무사)
영구 조치
①패치 버전으로 업그레이드 ②모든 키 교체 ③기계 CVE 모니터링
10.0
CVSS / 최악 등급
몇 달
패치되지 않은 기간
전체 환경
가정한 유출 범위
모니터링
가장 효과적인 예방책

“과금을 멈췄다” ≠ “처리했다”

청구를 멈추는 것은 임시 조치다. 유출 경로를 닫는 것은 별개의 작업이다. 둘 다 끝냈을 때 비로소 완료된 것이다.

무슨 일이 있었나 (타임라인)

  1. 0일째 — 출시·가동

    AI의 도움으로 만든 앱이 출시되어 프로덕션에서 가동 중이었다.
  2. 어느 날 — 요금 급증

    클라우드 AI 요금이 갑자기 치솟는다. 결코 쓸 일 없는 모델에서 대량 작업이, 자기 것이 아닌 일을 하고 있었다.
  3. 조사 — 제3자의 악용

    "우리 앱이 폭주했다"로는 설명되지 않는다. 제3자가 탈취한 키로 그것을 돌리고 있었다.
  4. 추적 — 유출 경로 조사

    "그럼 키는 어디서 유출됐나?" 실제 조사는 여기서 시작됐다.

처음에 잡히지 않은 이유

실수가 저질러진 순서대로 — 실수가 곧 교훈이기 때문이다.

1

원인을 너무 일찍 단정

첫 가정: "우리 폴백 코드가 오작동했다." 하지만 데이터(실제로 처리된 내용)는 그것일 수 없음을 분명히 했다.
→ "내 잘못"이나 "남의 잘못"을 정하기 전에, 먼저 데이터를 보라.
2

깨끗한 grep 결과로 조사를 끝낼 뻔했다

코드, git 히스토리, 연결된 저장소를 모두 검색 → 어디에도 평문 키 없음. "추적 불가"로 거의 포기할 뻔했다. 파일만 보다가 런타임 유출을 놓쳤다.
3

증상을 가리는 것을 고친 것으로 착각

프록시에서 증상을 가리니 거의 끝난 듯 느껴졌다. 하지만 가려도 RCE는 그대로 남는다. 원인을 고치기 전까지는 계속 악용될 수 있다.

깨끗한 grep은 안전 확인이 아니다

어떤 파일에도 키가 없더라도, 취약점은 실행 중인 프로세스에서 환경 변수를 빼낼 수 있다. 유출은 파일뿐 아니라 런타임(RCE, HTTP 헤더)에서도 일어난다. RCE란 무엇인가 참고.

실제 원인: 패치되지 않고 방치된 공개 CVSS 10.0

오래된 액세스 로그의 이상한 시그니처가 위협 인텔리전스와 곧바로 일치했다. 사용 중이던 웹 프레임워크 버전 범위에는 공개된 인증 전 RCE(CVSS 10.0) 가 있었고, 이미 실제로 악용되고 있었으며 — 몇 달간 패치되지 않은 채 가동되고 있었다.

  • 이것은 수동적인 버그가 아니라, 공격자가 서버에서 코드를 실행해 환경을 빼낸 것이었다.
  • 피해를 제한한 점이 하나 있었다: 실행은 권한 없는 컨테이너 내부에 머물렀다(호스트 root는 탈취되지 않았다). 침해 검토 결과 지속형 백도어, 마이너, C2는 없었고 — 확인된 피해는 시크릿 탈취였다.
  • 내부 DB에 도달 가능했기 때문에, 대응은 DB 내용도 유출됐다고 가정했다.
보이는 증상: 갑작스럽고 낯선 청구서
운영자에게 보이는 것은 여기까지
↑ 탈취된 키는 나중에 재판매·악용된다
환경 전체(모든 시크릿)가 빠져나간다
↑ 공격자가 가동 중인 서버에서 임의 코드를 실행한다
원인: 패치되지 않고 방치된 공개 CVSS 10.0(인증 전 RCE)
갑작스러운 청구서는 눈에 보이는 증상일 뿐이다. 그 아래에 원인, 즉 패치되지 않고 방치된 공개 CVSS 10.0이 있다.

교훈: 무언가 이상하게 동작하면, 먼저 알려진 CVE를 의심하라. 자기 버그라고 단정하지 마라.

유출 경로 좁히기: 그 키는 어디에 있었나?

grep으로 아무것도 나오지 않으면 찾는 방법을 바꿔라. 악용된 키가 적혀 있던 곳을 나열하면 유출 경로 후보가 줄어든다. 예를 들어 악용된 키가 운영 서버의 환경 변수에만 있었다면, Git이나 브라우저가 아니라 운영 서버나 호스팅 업체를 의심해야 한다.

유출되는 곳흔한 원인내 설정을 확인하는 법막는 법
Git 히스토리 / 공개 저장소.env 커밋, 키 하드코딩gitleaks git으로 전체 히스토리를 스캔. GitHub 시크릿 스캐닝 알림 확인먼저 폐기하고, 그다음 커밋 전에 막는다
브라우저로 전송되는 JavaScriptNEXT_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의 ^가 아니다.

내 설정을 점검하는 법

이상한 청구서를 막 발견했든 아직 아무 일도 없든, 다음 순서로 확인할 수 있다. 모든 단계는 내 계정과 서버만 본다.

1

어느 키가 언제 쓰였는지 본다

AI API 대시보드에서 사용량과 비용을 프로젝트별(Anthropic은 Workspace별), 날짜별로 나눠 보라. 쓰지 않는 모델이나 내 작업이 돌지 않는 시간대의 사용량이 있다면 그 키가 유출된 것이다. 그다음 그 키가 저장되어 있던 곳에 해당하는 표의 행부터 시작하라.
2

브라우저로 전송되는 파일을 확인

Next.js에서는 NEXT_PUBLIC_으로 시작하는 환경 변수의 값이 빌드 시 브라우저로 보내는 JavaScript에 그대로 삽입된다. 빌드한 뒤 결과물에서 키의 접두어를 검색하라. 예: grep -rl "sk-" .next/static. 여기서 발견된 것은 누구나 읽을 수 있었던 것이다.
3

시크릿 파일이 공개 폴더에서 접근되지 않는지 확인

내 사이트를 대상으로 curl -sI https://example.com/.env를 실행하고(example.com을 내 도메인으로 바꿔서), 첫 줄의 상태가 200이 아닌지(404나 403이어야 한다) 확인하라. 자세한 내용은 공개 디렉터리의 시크릿 파일 점검에 있다.
4

운영 버전을 권고문과 비교

위 상자처럼 운영 컨테이너 안에서 npm ls를 실행해 버전을 확인하라. osv-scanner scan -L pnpm-lock.yaml은 락파일에 알려진 취약점이 있는 버전이 포함되어 있는지 확인한다.
5

로그에 시크릿이 있는지 확인

앱의 로그가 저장된 폴더에 gitleaks dir --redact를 실행해 로그에 기록된 키를 찾아라. --redact는 발견된 값 자체가 화면에 나오지 않게 한다.
6

한도와 알림 확인

OpenAI는 대시보드의 한도 페이지에서 지출 알림과 월간 하드 지출 한도를 설정할 수 있다. Anthropic은 Workspace별 지출 한도를 설정할 수 있다. 아무것도 멈추지 않는 알림만 있는 상태가 아닌지 확인하라.

가장 먼저 한 일 (재현 가능한 절차)

1

악용된 키를 즉시 폐기

피해를 멈추지만, 일의 일부일 뿐이다.
2

프레임워크를 패치 버전으로 업그레이드

실제로 RCE를 닫는다. 업그레이드 후 로그에서 유출 시그니처가 사라졌는지 확인한다.
3

모든 시크릿 교체

유출 가정. "어디서든 악용 가능한 것부터" 순서로: 오브젝트 스토리지 키 → 세션 서명 키 → API 키 → DB 자격증명.
4

침해 검토

지속성, 악성 cron, 외부 C2가 있는지 점검(여기서는 없음).
5

계정 측 방어

비밀번호 변경, MFA 활성화, 낯선 API 키가 있는지 확인.

예방: 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였다. 올바른 원인은 첫 시도에 찾은 것이 아니라, 여러 번 틀리고 다시 확인하는 과정에서 찾았다.

다음으로 읽기

출처

FAQ

QAPI 키가 유출됐다면, 악용된 그 하나의 키만 폐기하면 충분한가요?
A

아니요. 유출 경로가 런타임(RCE나 헤더 유출)에 있다면 환경의 모든 시크릿이 한꺼번에 유출됐다고 가정하고 전부 교체하세요. 악용이 확인된 키는 유출된 것의 일부일 뿐입니다.

Q코드와 git 전체를 grep 해도 키가 나오지 않으면 안전한가요?
A

반드시 그렇지는 않습니다. 어떤 파일에도 키가 없더라도, 프레임워크 취약점이 실행 중인 프로세스에서 환경 변수를 직접 빼낼 수 있습니다. 유출은 파일뿐 아니라 런타임에서도 일어납니다.

Q이 일이 다시 일어나지 않게 하는 가장 확실한 방법은 무엇인가요?
A

의존성을 기계로 CVE 모니터링하는 것입니다. 여기서의 원인은 사람이 공개된 CVSS 10.0을 몇 달간 놓친 것이었습니다. Dependabot이나 osv-scanner는 이를 자동으로 잡아냅니다.

QAI API 요금이 갑자기 늘었습니다. 무엇부터 해야 하나요?
A

먼저 제공자의 대시보드에서 악용된 키를 비활성화해 피해를 멈추세요. 그다음 그 키가 저장되어 있던 곳에서 유출 경로를 좁히고, 취약점이나 잘못된 설정을 고친 뒤에야 같은 곳에 있던 모든 시크릿을 교체합니다. 키 폐기만으로는 유출 경로가 열린 채로 남습니다.

Q어느 키가 유출됐는지 어떻게 알 수 있나요?
A

AI API 대시보드에서 사용량과 비용을 프로젝트별(Anthropic은 Workspace별)로 나눠 보세요. 쓰지 않는 모델이나 시간대의 사용량을 만든 키가 유출된 키입니다. 서비스와 환경마다 키를 따로 쓰고 있다면, 그 키가 저장된 장소가 곧 유출 경로를 알려 줍니다.

Q예산 알림을 설정하면 부정 과금이 멈추나요?
A

반드시 그렇지는 않습니다. 예를 들어 Google Cloud는 알림만 있는 예산은 사용량이나 지출을 자동으로 제한하지 않는다고 밝히고 있습니다. 실제로 사용을 멈추려면 OpenAI의 월간 하드 지출 한도나 Anthropic의 Workspace별 지출 한도처럼 사용을 차단하는 한도를 설정하세요.