Jaejinally
dev··21분 읽기

내 API 키가 털렸다

앱에 심어둔 API 키가 해커에게 털려서 20만엔 넘는 청구서가 날아왔다. 1인 개발자가 겪은 보안 사고의 기록.

3월 10일, 화요일 밤

평소처럼 이번 달 클라우드 비용을 확인하려고 콘솔에 접속했다.

매달 초에 지난달 결제 금액을 확인하는 게 습관이었다. 내 앱은 영수증을 찍으면 AI가 자동으로 가계부에 기록해주는 서비스인데, 유저가 100명 정도라 월 비용은 거의 0에 가까웠다. 2월 청구액이 ¥97이었으니까. 커피 한 잔도 안 되는 돈.

근데 3월 청구 예상액을 보는 순간, 뭔가 이상했다.

¥217,761.

잠깐. 뭐지 이게.

처음엔 콘솔 버그인 줄 알았다. 표시 오류겠지 싶어서 새로고침도 해봤다. 근데 상세 내역을 열어보니까 전부 AI API 사용료였다. 하루에 수천 건, 많을 때는 하루에 2만 건이 넘는 요청이 찍혀 있더라.

내 앱 유저는 100명이다. 무료 플랜 기준 하루 스캔 15회. 100명이 전부 풀로 써도 하루 최대 6,000건인데, 실제 요청은 하루 400만 건을 넘기고 있었다.

그 순간 알았다. 이건 내 앱 문제가 아니다. 털렸다.

10일 동안 아무것도 몰랐다

나중에 로그를 역추적해보니까, 공격은 2월 28일부터 이미 시작돼 있었다. 처음에는 하루 몇백 건으로 조심스럽게 키를 테스트하더니, 3월 들어서 본격적으로 폭주하기 시작한 거다.

공격 기간 동안의 일별 트래픽을 정리하면 이렇다.

일별 총 요청량 (과금 + rate limit + 에러 전부 포함)
──────────────────────────────────────────────────────

2/28  ▓                                          10,276
3/01  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                   910,032
3/02  ▓▓▓▓▓                                     203,464
3/03  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                          619,951
3/04  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   1,423,385
3/05  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                         651,405
3/06  ▓▓▓▓▓▓▓▓▓                                 347,444
3/07  ▓▓▓▓▓                                     208,473
3/08  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 4,115,858  ← 피크
3/09  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   2,839,314
3/10  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   1,430,000  ← 13:40 UTC 키 폐기

      총 11,329,946건. 그중 과금된 건 107,042건.
      내 앱에서 나간 건 268건.

3월 8일이 피크였다. 하루 동안 411만 건. 그중 실제로 과금된 건 24,237건이고, 나머지 400만 건 가까이는 구글 쪽 rate limit에 걸려서 막혔다. 과금된 것만으로도 이미 엄청난 금액이었다.

10일 동안 전혀 몰랐다. 이유는 단순하다. 알림을 안 걸어놨다. 결제 알림도 없고, 이상 트래픽 감지도 없고, 쿼터 제한도 안 걸어놨다. 매달 ¥100도 안 나오는 프로젝트에 무슨 알림이 필요한가 싶었으니까. 지금 생각하면 진짜 멍청한 판단이었다.

어떻게 털렸나

원인은 허무할 정도로 단순했다.

내 앱은 iOS 앱이고, 앱 안에 클라우드 서비스 설정 파일이 들어간다. 거기에 API 키가 포함되어 있다. 이건 모바일 개발에서는 완전 표준적인 구조다. 클라우드 서비스 공식 문서에도 "클라이언트 앱에 포함된 API 키는 안전합니다"라고 적혀 있다. 나는 그걸 믿었다.

근데 함정이 있었다.

데이터베이스나 인증 쪽 API 키는 보안 규칙이 별도로 걸려 있어서 키만 가지고는 실제로 아무것도 못 한다. 근데 AI API는 이야기가 달랐다. 키 하나면 인증 없이 바로 호출이 된다. 거기다 내 키는 "iOS용 키"라는 이름으로 자동 생성된 건데, 열어보니까 실제로는 아무런 제한이 안 걸린 범용 키였다. iOS 번들 ID 제한도 없고, API 제한도 없고. 그냥 만능 열쇠.

누군가가 내 앱 파일을 역공학해서 이 키를 꺼낸 거다. IPA 파일에서 설정 파일 추출하는 건 알 사람은 다 아는 기본적인 기법이다. 거창한 해킹도 아니다.

공격 경로
──────────────────────────────────────────────────────

[App Store]                    [해커의 서버 — 미국/유럽]
    │                                │
    │  1. 앱 다운로드                  │
    ▼                                │
[IPA 파일]                           │
    │                                │
    │  2. 역공학 → GoogleService      │
    │     -Info.plist에서             │
    │     API 키 추출                 │
    ▼                                │
[API 키: AIzaSyDQ...1_JU]           │
    │                                │
    └──────── 3. 키 전달 ────────────▶│
                                     │
                                     │  4. 봇이 24/7 API 직접 호출
                                     │     POST /v1beta/models/
                                     │     gemini:generateContent
                                     ▼
                              [Google AI API]
                                     │
                                     │  5. 과금 → 내 계정
                                     ▼
                              [¥217,761 청구서]

요청의 99.7%가 미국과 유럽에서 들어왔다. 내 앱은 일본하고 한국 유저밖에 없는데. 새벽 3시에도 24시간 쉬지 않고 요청이 계속 들어오고 있었다. 사람이 하는 게 아닌 건 너무 명백했다.

혼자서의 전쟁

발견하자마자 바로 키를 폐기했다. revoke 누르니까 분당 3,000건씩 들어오던 요청이 1분 만에 0이 됐다. 솔직히 이 순간이 제일 소름 돋았다. 진짜 누군가가 저 너머에서 내 키로 봇을 돌리고 있었구나 하는 게, 숫자로 눈앞에서 증명되니까.

키 폐기 전후 — 분 단위 요청량 (3/10, UTC)
──────────────────────────────────────────

13:39  ████████████████████████████████  3,074
13:40  ███████████████████████████████   2,731
13:41  ████████████████████████████████  2,963
13:42  ██████████████████████████        2,330
13:43  ████                                342  ← API 키 revoke
13:44  ·                                     0
13:45  ·                                     0

근데 키를 끊은 건 시작에 불과했다.

내 앱 코드에 무한 루프가 있는 건 아닐까. 서버 코드가 미친 듯이 재시도를 하고 있는 건 아닐까. 이런 의심이 계속 들었다. 왜냐면 클라우드 서비스 측에 환불을 요청하려면 이게 100% 외부 공격이라는 걸 증명해야 했으니까. "아마 해킹인 것 같아요"로는 안 된다. 확실한 증거가 필요했다.

여기서부터 포렌식이 시작됐다.

서버 함수 호출 로그를 전부 뒤졌다. 결과는, 공격 기간 동안 서버 함수 호출이 0건이었다. 공격자는 내 서버를 완전히 우회해서 API를 직접 때리고 있었다는 뜻이다.

내 앱에 자체적으로 만들어둔 비용 추적 시스템을 확인했다. 2월과 3월을 합쳐서 총 268건, 비용 $0.09. 근데 클라우드 모니터링에 찍힌 성공 요청은 107,042건. 이 차이가 곧 외부 공격의 증거다. 106,774건은 내 앱에서 나간 게 아니다.

요청 크기도 완전히 달랐다. 내 앱은 영수증 이미지를 보내니까 요청 하나가 평균 362KB인데, 공격 기간의 요청 중간값은 119KB. 이미지가 아니라 텍스트 프롬프트를 보내고 있었다는 뜻이다. 내 앱의 영수증 스캔 기능과는 완전히 다른 사용 패턴.

이런 걸 하나하나 모아서 인시던트 리포트를 썼다. 증거 항목만 12개, 총 수십 페이지. 거의 매일 새벽 2시까지 잠을 못 잤다.

혼자서.

1인 개발이라는 게, 이런 일이 터지면 진짜 답이 없다. 물어볼 사람이 없다. 대신 해줄 사람도 없다. 새벽에 혼자 로그 뒤지면서 이 정도면 환불받을 수 있을까, 안 되면 어떡하지, 이런 생각이 머리에서 안 떠났다.

근데 좀 이상한 말일 수 있는데, 이런 벽이 결국 나를 성장시켜주는 거라고 생각한다. 이번 일 아니었으면 보안에 대해 이렇게까지 깊이 생각해본 적이 있었을까. 솔직히 지금까지 너무 안일했다.

Google Cloud 서포트와의 대화

증거를 다 모아서 Google Cloud 빌링 팀에 연락했다. 사건 당일인 3월 10일 밤, 라이브 채팅으로.

Case #68854816·Unauthorized bot abused my Gemini API key
Google Cloud Support, Saniya — Mar 10, 2026 20:59 IST
Thank you for contacting Google Cloud Support. My name is Saniya and I'll be working with you today.
Jaejin Park
Hello Saniya, nice to meet you too! I've already sent a detailed message in the case history and attached my full incident report. To summarize briefly: a malicious bot abused my Gemini API key between Feb 28 – Mar 10, charging ¥217,761 to my account. My actual app usage was only $0.09. I'm a student developer and I'm unable to pay this amount.
Saniya
Is it okay, If I take 7-8 minutes to review the details from my end?
Saniya
Upon reviewing your billing account, I see the charges for "Gemini API" service. Just to clarify, do you want to keep using Google Cloud services and keep your project active, or would you like to disable billing to stop accumulating charges?
Jaejin Park
Yes, I would like to keep my project active. However, I've already: 1) Revoked the compromised API key, 2) Disabled the Generative Language API entirely, 3) Verified that all unauthorized traffic has completely stopped since March 10. My concern is only about the unauthorized charges (¥217,761) that were already incurred.
Saniya
Please note, you will be liable for all further charges on your account as you wish to keep your project active. I have gone ahead and filed a billing adjustment request for the charges on your account. Our specialized team will review and revert within 2-3 business days.

여기까지는 희망이 있었다. 케이스가 접수됐고, 전문팀이 검토해준다고 했다. 이틀 뒤, 내 쪽에서 추가 증거를 보냈다.

Re: Case #68854816·Critical Update on Root Cause
Jaejin Park — Mar 12, 2026 09:32 JST
1. Exact Root Cause Identified: The breached key was auto-generated by Firebase and labeled as "iOS key." However, I discovered it was created as an UNRESTRICTED key by default, lacking the iOS bundle ID restriction. 2. 100% Security Overhaul Completed (53 Steps Taken): Deleted all vulnerable API keys. Completely removed client-side API calls — all AI processing is now strictly handled via server-side Cloud Functions. Enforced Firebase App Check across the entire project.

이틀 더 뒤에 답이 왔다.

Billing Adjustment Result · Mar 12, 2026
Saniya — Google Cloud Billing Support
I'm pleased to inform you that our specialized team has completed their evaluation and approved a partial adjustment of the requested amount. The total requested adjustment was ¥218,611 JPY. The approved adjustment is ¥163,958 JPY. The remaining outstanding balance on your account is ¥60,346 JPY.

75% 면제. 일단 숨은 쉴 수 있게 됐다. 감사했다.

근데 남은 ¥60,346도 내 사용분이 아니다. 내 실제 사용료는 ¥14다. 나머지는 전부 해커가 만든 트래픽이다.

바로 재심을 요청했다.

Appeal · Mar 13, 2026
Jaejin Park
I am a university senior, and my graduation is just a week away, on March 20, 2026. I recently had to empty all of my savings to cover moving expenses. Although I start working on April 1st, I will not receive my first paycheck until the end of April. I currently have absolutely zero financial margin to pay the remaining ¥60,346 JPY.
Saniya — Mar 14, 2026
I sincerely apologize for the delay. We understand you were expecting a full billing adjustment, and I truly apologize that we couldn't meet that expectation. This case will now be marked as resolved.

이걸로 끝낼 수는 없었다. 감정이 아니라 논리로 다시 써야 한다고 생각했다.

Structured Appeal — Reopened Case · Mar 16, 2026
Jaejin Park
This is not a restatement of my previous request. I am presenting structured technical arguments. My application's total legitimate usage for the entire billing period was 268 API calls totaling $0.09 USD (approximately ¥14 JPY). If 75% of the unauthorized charges warranted adjustment, the remaining 25% warrants the same — the unauthorized nature of the traffic does not change based on what percentage it represents. Firebase's security documentation explicitly states: "You do not need to treat API keys for Firebase services as secrets, and you can safely embed them in client code." I followed this guidance exactly.
Saniya — Mar 15, 2026
I completely understand your concern here. I have submitted another adjustment request as a one-time courtesy, subject to approval. Our specialized team will carefully review the request, and you can expect an update via email within the next 2-3 business days.

지금 이 글을 쓰고 있는 시점에서, 2차 조정 결과를 기다리고 있다.

현재 상황 정리
──────────────────────────────────────────

총 청구액          ¥218,611
1차 면제           ¥163,958  (75%)  ✓ 승인
남은 잔액          ¥ 60,346  (25%)  ⏳ 2차 조정 대기 중
내 실제 사용료      ¥     14

2차 결과 예상 회신일: ~3/18 (수)

(결과가 나오면 여기를 업데이트하겠다.)

이후에 바뀐 것들

이 사건 이후로 개발 습관이 근본적으로 달라졌다.

키는 이제 절대 클라이언트 앱에 넣지 않는다. 모든 AI API 호출은 서버를 경유하도록 바꿨다. 앱이 서버 함수를 호출하고, 서버 함수가 유저 인증을 검증한 다음에 AI API를 대신 호출하는 구조다. 앱 안에는 AI API를 직접 호출할 수 있는 키가 아예 존재하지 않는다. 누가 앱을 역공학해도 쓸 수 있는 키가 없다.

변경 전 (사고 당시)
──────────────────────────────────────────

iOS 앱  ──── API 키 포함 ────▶  Google AI API
                                    │
         해커도 같은 키로 ────────▶  │  ← 구분 불가
                                    ▼
                              내 계정에 과금


변경 후 (현재)
──────────────────────────────────────────

iOS 앱  ──── Firebase Auth ────▶  Cloud Function
          (키 없음, 토큰만)         │
                                   │ 1. 유저 인증 검증
                                   │ 2. App Check 검증
                                   │ 3. 일일 사용량 확인
                                   │
                                   ▼
                              Google AI API
                              (서버만 접근 가능)

         해커가 앱을 역공학해도
         AI API에 접근할 키가 없음  ✕ 차단

결제 알림은 $10, $30, $50 단위로 걸어뒀다. 이걸 처음부터 했으면 공격 시작 하루 만에 잡을 수 있었을 텐데, 하는 후회가 제일 크다.

비정상 트래픽이 감지되면 자동으로 모든 AI 서비스를 정지시키는 킬스위치도 만들었다. 수동으로도 원클릭에 끌 수 있게. 이건 앞으로 새로운 기능을 기획할 때도 기본으로 들어갈 예정이다.

키는 용도별로 분리했다. 이 키는 이 API만, 이 플랫폼에서만 쓸 수 있도록 제한을 건다. 범용 키 하나로 전부 처리하는 건, 집 문 하나에 마스터키 하나만 두는 거랑 다를 게 없다.

이 글을 읽는 사람에게

사실 이건 매우 초보적인 실수다. 경험 있는 개발자가 보면 당연한 얘기일 수 있다.

근데 나처럼 처음 앱 만들어서 스토어에 올려본 사람은 이런 걸 모른다. 공식 문서에 "키를 클라이언트에 넣어도 안전합니다"라고 적혀있으면 그걸 믿는다. 그게 어떤 API에는 맞고 어떤 API에는 안 맞는다는 뉘앙스를, 첫 프로젝트에서 캐치하기란 쉽지 않다.

하나만 기억해줬으면 좋겠다.

앱에 API 키를 넣는 순간, 그 키는 전 세계에 공개된 거다. IPA든 APK든 누구나 역공학할 수 있다. 넣어야 하는 키라면 그 키로 할 수 있는 일을 최소한으로 제한해야 하고, 과금이 발생하는 API는 반드시 서버를 거쳐서 호출해야 한다.

보안은 "나중에 하자"가 아니라 코드 첫 줄부터 같이 가는 거라는 걸, 20만엔 주고 배웠다. 비싼 수업료였다.


이 글은 실제 진행 중인 사건을 바탕으로 작성했다. 환불 결과가 나오면 업데이트할 예정이다.

다음 글에서는 이 사고 이후 FinPal의 아키텍처를 서버 사이드 프록시 구조로 전면 개편한 과정을 기록하겠다.

관련 글

life2026년 3월 16일

이 블로그를 시작하는 이유

왜 2026년에 개인 블로그를 시작하게 됐는지, 이 블로그에서 어떤 이야기를 하려고 하는지 적어봤어.

새 글을 이메일로 받아보세요

매주 새로운 개발 이야기, 여행기, 일상 에세이를 이메일로 배송해드립니다.