개발··20분 읽기

내 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. 역공학 → API 키 추출
API 키: AIzaSyDQ...1_JU
3. 키 탈취
해커의 서버 (미국/유럽)
4. 봇이 24/7 API 직접 호출
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

Saniya — Google Cloud SupportMar 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 ParkMar 10, 2026

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 ParkMar 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 SupportMar 12, 2026

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 ParkMar 13, 2026

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.

SaniyaMar 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 ParkMar 16, 2026

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.

SaniyaMar 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차 조정 결과는 예상보다 빨리 왔다.

2차 조정 결과

Mar 16, 2026

Saniya — Google Cloud Billing SupportMar 16, 2026

I completely understand your concern regarding the adjustment request for remaining ¥24,276 JPY. I want to assure you that I have personally advocated for your case with our specialized team to explore every possible option for a waiver. Despite these extensive efforts and my personal push for an exception, the specialized team has confirmed that we are unable to proceed with extra adjustment, as the usage aligns with our established billing policies. I sincerely apologize for this outcome.

중간에 2차 부분 면제가 조용히 적용되어 있었다. ¥60,346에서 ¥24,276으로 줄었다. 하지만 그 이상은 안 된다는 최종 통보.

최종 결과
총 청구액
¥218,611
1차 면제
¥163,958
2차 면제
¥36,070
총 면제율
91.5%
최종 본인 부담
¥24,276
내 실제 사용료
¥14

면제 ¥200,028 / 부담 ¥24,276 — 케이스 종결

이후에 바뀐 것들

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

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

변경 전 (사고 당시)

정상 요청

iOS 앱
API 키 포함
Google AI API
과금
내 계정

해커 요청 — 구분 불가

해커
같은 API 키
Google AI API
과금
내 계정
변경 후 (현재)

정상 요청

iOS 앱
토큰만
Cloud Function
인증 + App Check
Google AI API

해커 요청 — 차단

해커
키 없음
BLOCKED

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

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

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

이 글을 읽는 사람에게

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

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

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

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

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

끝나고 나서

¥24,276. 약 22만 원.

91.5% 면제를 받았다. 고마운 일이다. Saniya는 끝까지 내 편에서 싸워줬다. 하지만 남은 ¥24,276도 내가 쓴 게 아니다. 내 실제 사용료는 ¥14다. ¥24,262는 해커가 만든 트래픽 비용이고, 그걸 내가 내야 한다.

납득이 되냐고 물으면, 솔직히 안 된다.

근데 이것도 결국 내 책임이다. 키를 열어둔 건 나였고, 알림을 안 건 것도 나였고, 10일 동안 모른 것도 나였다. 해커가 나쁜 건 맞지만, 문을 잠그지 않은 건 나니까.

졸업이 3일 뒤다. 4월 1일에 입사하면 첫 월급은 4월 말에 나온다. 지금 통장 잔고로는 이사 비용이 빠듯한 상황에서 22만 원이 추가로 날아갔다. 학생 개발자한테 이 돈은 작지 않다.

그래도 이걸 "수업료"라고 부르기로 했다.

20만 엔짜리 보안 수업. 어떤 부트캠프보다 효과가 확실했다. API 키 관리, 서버 사이드 프록시, 결제 알림, 킬스위치, 포렌식 리포트 작성법까지. 이 모든 걸 일주일 만에 독학했다. 그것도 실전으로.

이 글을 공개하는 이유는 단순하다. 나 같은 실수를 할 사람이 분명히 있을 거니까. 그 사람이 이 글을 읽고 키 하나라도 제한을 걸어두면, 이 22만 원은 의미가 있다.


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

관련 글

개발2026년 4월 2일

사고 도구에 AI가 꼭 필요한가

모든 프로덕트에 AI를 붙이는 시대. 하지만 Dimension은 AI 없이도 작동하는 사고 도구를 지향한다. 왜 그런 선택을 했는지, 그리고 이건 노트 앱과 어떻게 다른지.