App 從來沒上架,帳單先衝上來了
一把一年前在 Firebase 產生、給一個最後沒上架的 iOS app 用的 Gemini API key,10 月 2 日單日被打出 $3,909 的用量。app 不在線上,key 卻一直活著——而且預設不綁 app、不綁 API、不綁來源。
- 一把約一年前在 Firebase 產生、給一個從未上架的 iOS app 用的 Gemini API key,2026 年 10 月 2 日單日被打出 $3,909 的用量(扣抵免後 $3,901);前一天的帳單幾乎是零。
- key 從哪裡流出去的還不知道。能確定的是:這種 key 產生時預設不綁 app、不綁 API、不綁來源、沒有用量上限,任何拿到它的人都能直接用你的帳單呼叫 Gemini。
- 從零到單日數千美元、中間沒有爬坡,這個形狀比較像 key 被收進轉賣代理的「key 池」:不是一個人在用,是一群人同時在消耗。
帳單先長出一根柱子
想像你一年前辦了一張副卡,準備給一間後來沒開成的店用。卡一次都沒刷過,你也忘了它的存在。10 月 2 日,它在一天之內刷掉相當於十二萬台幣。你是看對帳單才知道的。
這次的副卡是一把 Gemini API key。約一年前(2025 年),我們在 Firebase 主控台替一個開發中的 iOS app 產生了它;app 後來沒有送審、沒有上架,專案停了,key 留在原地。接下來一年,這把 key 的消耗是零。2026 年 10 月 2 日,費用頁長出一根柱子:
費用 省下的費用 總費用 $3,909 − $8.15 = $3,901 10月1日:趨近於 0 10月2日:單日 $3,909
這是自家事件,所以這篇的第一手不是新聞稿,是我們自己的帳單頁。也因為是自家事件,哪些查完了、哪些還沒查,下面會照證據三級誠實擺:撰稿的此刻(10 月 3 日),「怎麼流出去的」還是一個空格。
- 約 2025 年(一年前)在 Firebase 主控台產生 Gemini API key,供開發中的 iOS app 使用;app 之後未送審、未上架GCP 憑證頁的金鑰建立時間;App Store Connect 無送審紀錄
- 未知key 在某個時點、以某種形式外流,被第三方取得洩漏途徑與時間點尚未查出;repo 歷史掃描進行中
- 2026-10-01帳單如常:當日費用趨近於零GCP 費用頁報表
- 2026-10-02單日費用暴增至 $3,909(扣抵免後 $3,901),來源是 Gemini API 呼叫GCP 費用頁報表與截圖
- 2026-10-03看費用頁時發現異常,作廢金鑰,開始追查洩漏點並準備退款申請本站處置紀錄
時間線最貴的不是最後兩格,是第二格:外流到爆量之間隔了多久,現在還沒有答案。
發現它的不是告警,是帳單頁
這起事件的發現方式,說出來不太體面:是人打開費用頁看到的,時間是 10 月 3 日——暴量發生的隔天。10 月 2 日當天,沒有任何通知把這件事推到我們眼前。
一把休眠的 key 開始被大量使用,其實是雲端環境裡最容易偵測的異常之一:基準是零,任何非零都是訊號。預算警報是免費功能,五分鐘可以設完;設了,10 月 2 日上午就會收到第一封信,當天就能把 key 作廢,帳單可能停在三位數。沒設,就是現在這樣,用整整一天的時差換一根 $3,909 的柱子。這一段沒有別人可以檢討,訊號不是沒有出現,是沒有人安排自己去收。
攻擊者怎麼做到
到這裡,其實已經足夠理解整起事件。如果你關心的是該怎麼避免,可以直接往「如何避免」繼續閱讀。如果你想知道我們查到哪裡、還缺哪些證據,下面開始進入技術拆解。
白話結論:攻擊者沒有「攻破」任何主機。他在某個地方撿到一把還有效的 key,而這把 key 天生就是裸的——產生的當下,Google 不會要求你綁定 app、限制 API 或設用量上限,預設全部是空白。拿到字串,就等於拿到計費權。
深入閱讀對照 MITRE ATT&CK:三格裡第一格全空取得途徑是 gap,變現那一格反而證據最完整
三格裡最該有答案的第一格目前全空。這不是修辭上的保留:在掃描結果出來之前,任何「一定是 commit 進 GitHub 了」的說法都只是把最常見的情境當成本案的證據。
這次外洩了什麼
外洩的憑證只有一把,但它值得整張表,因為它的「權限範圍」一欄就是整起事件的根因。
| 服務 | 憑證類型 | 用途 | 存在形式 | 放在哪 | 權限範圍 | 狀態 |
|---|---|---|---|---|---|---|
| Google Gemini API(Generative Language API) | API key(AIza 開頭格式,已遮蔽) | 開發中的 iOS app 呼叫 Gemini 功能 | 明文字串(開發期存在於程式碼與設定中) | 產生於 Firebase 主控台;實際外洩位置未確認 | 產生時預設無任何限制:不綁 bundle ID、不綁 API、不綁來源 IP、無用量上限——持有字串即可對專案計費 | 2026-10-03 已作廢 |
表裡最該看的是 scope 那一欄:這把 key 的能力從頭到尾沒有變過,變的只是持有它的人。
我們怎麼處理
10 月 3 日看到帳單的當下,第一個動作只有一個:到 GCP 憑證頁把這把 key 作廢。沒有任何正式環境在用它,作廢是零風險、即時止血的一步;key 失效的那一刻起,池子裡所有人的請求都會開始收到 401。
接下來排的三件事,到撰稿為止都還在進行:調費用報表的 SKU 明細與 API 請求曲線,把異常時段鎖清楚;對當年那個 iOS app 的 repo 跑完整歷史的金鑰掃描,找洩漏點;整理時間線與證據,向 Cloud Billing 支援開退款 case。同時補上本來就該有的預算警報。這篇會在洩漏點與退款結果出來之後更新——哪一格從 gap 變成 proven,會照實改。
哪裡做對,哪裡做錯
做對的只有止血速度:發現之後沒有先去追「是誰」,先把 key 作廢。追查可以慢慢來,計費不會等你。
做錯的至少有三層,而且每一層都和攻擊者無關。第一層是 key 的出生:產生的當下沒有加任何限制,這把 key 從第一天起就是「撿到就能用」的狀態。第二層是 key 的退場:app 停止開發的時候,沒有人回頭作廢它的憑證——專案會結束,金鑰不會,它只是回到抽屜裡繼續有效。第三層是偵測:一個幾乎沒有在動的計費專案,連一條免費的預算警報都沒有設,於是唯一的偵測手段是「有人剛好打開帳單頁」。三層防線,每一層的成本都接近零,每一層都空著。
如何避免
當天就能做的是兩件事。把帳上每一把 API key 列出來,看建立時間和限制;restrictions 空白、又想不起來用途的,直接作廢或補上限制。然後給每個會計費的專案設預算警報——金額設多少都好,重點是讓「異常發生的那一天」就有信進來。
一週內做的是把「key 的出生規格」定下來:新 key 一律同時加 API restriction(只准它該呼叫的那一個 API)與 application restriction(綁 bundle ID、網域或 IP),再設單日用量上限。限制不會讓 key 不外洩,但會把一把外洩 key 的價值從「池子裡的商品」砍成「幾乎不能用的字串」。
這一季做的是改架構和改流程。行動 app 要呼叫 Gemini,走 Firebase AI Logic 加 App Check,讓裸 key 從頭到尾不出現在 client 程式碼裡。專案結案的 checklist 加一條「作廢所有專用憑證」,讓 key 的生命週期跟著專案一起結束,而不是跟著遺忘一起延長。
這篇提到的項目要花多少錢
這張表的重點不是損失多大,是左右兩邊的對比:攻擊者與防守方的成本都趨近於零,差別只在誰先動手。
| 項目 | 用量 | 單價 | 期間 | US$ | NT$ | 舉證 |
|---|---|---|---|---|---|---|
| 2026-10-02 單日 Gemini API 消耗 | SKU 明細調閱中 | — | 單日 | 3901.00 | 124,832 | GCP 費用頁:費用 $3,909 − 抵免 $8.15 = $3,901(截圖存證) |
| 這把 key 休眠期間的消耗 | app 未上架,趨近於零 | — | 約一年 | 0.00 | 0 | GCP 費用歷史報表 |
| 攻擊者取得 key 的成本 估:掃描與驗證工具皆是現成的 | 掃描公開來源撿現成 | 趨近 US$0 | 一次性 | 0.00 | 0 | 本站估算,非本案直接證據 |
| 預算警報+API key 限制+用量上限 | 每個專案 | GCP 內建功能,US$0 | 持續 | 0.00 | 0 | Google Cloud 文件:Budgets 與 API key 限制皆不另外收費 |
| 追查與退款申請的人力 估 | 掃描 repo 歷史、調報表、寫 case | 估 4–8 小時 | 本次事件 | — | — | 工時紀錄尚未結算 |
| 退款(billing adjustment)能拿回多少 | case 準備中 | — | 未來數週 | — | — | 尚未送出;核退與否及金額由 Google 決定 |
| 合計(僅可證明列) | 3901.00 | 124,832 | ||||
proven 列相加是 $3,901——而表裡那一列本來能把它擋下的東西,單價是零。
你可以學到什麼
站在攻擊者那一側看,這是一門幾乎沒有成本的生意。他不挑目標、不研究你是誰,只是用現成工具持續掃公開來源,撿 AIza 開頭的字串,自動驗證哪些還活著。活著的 key 進池子,池子接上轉賣代理,賣給想要便宜 AI 額度的買家。對他來說,你的 app 有沒有上架、專案還有沒有在做,完全不重要;他唯一在乎的是那把 key 背後接著一張有效的信用卡。「這個專案沒什麼好偷的」在這門生意裡不成立——能計費,就是全部。
跟我們的防暴筆記對照
對照 INC-2026-0827(排程回報「找不到帳戶」,被停權的是整個專案)。相同的地方是憑證的年紀:0827 那把是 2020 年建的服務帳戶金鑰,這次是放了一年的 API key,兩把都在「沒有人記得它」的狀態下保持有效,也都是別的訊號先把事情捅出來——上次是雲端服務商停權,這次是帳單。不同的地方是這次更快也更純粹:沒有入侵、沒有後門、沒有主機被碰,從頭到尾只是一個字串換了持有人。防線也因此更簡單——上次要談 IAM 權限收斂,這次只需要出生時的三個限制和一條免費警報。
- 01專案會結束,金鑰不會。沒有退場程序的憑證不是被遺忘,是被留在原地等下一個持有人。
- 02一把 API key 的危險程度在產生那一刻就決定了:不綁 app、不綁 API、不設上限的 key,外洩等於直接交出計費權。
- 03基準為零的專案是最容易監控的——任何非零都是異常。連這種專案都沒有警報,有流量的專案更不會有人看。
- 04暴量的形狀本身就是證據:休眠後單日垂直起飛,指向的是池化轉賣,不是某個人撿去玩玩。
- 05在掃描結果出來之前,「一定是 commit 上 GitHub 了」只是把最常見的情境當成自己的證據。空格要誠實留著。
- 01AIza 開頭的 Google API key這種 key 是裸的持有即用憑證:沒有密碼、沒有簽章,字串本身就是計費權。
- 02API key restriction兩層限制——API restriction 限制它能呼叫什麼、application restriction 限制誰能用它;本案那把 key 兩層都是預設的空白。
- 03休眠憑證一年沒人用的 key 不會自己失效,它只是在等一個會用它的人;本案的根因就是這四個字。
- 04key 池(API key pool)偷來的 key 被集中轉賣的形式;解釋了為什麼消耗是單日垂直起飛而不是涓滴。
- 05預算警報(budget alert)本案唯一差一步就能把損失砍一個數量級的免費功能:差別是 10 月 2 日知道,還是 10 月 3 日看帳單才知道。
- 06用量上限(per-key quota)就算 key 外洩,單日上限決定那天的帳單是三十塊還是三千塊。
- 07gitleaks/trufflehog掃 git「完整歷史」找金鑰的工具;repo 轉私有或刪掉,救不回已經被收割過的歷史 commit。
- 08GitHub secret scanning公開 repo 出現已知格式的金鑰時,GitHub 會通報發行商處理;沒收過通知不代表沒外洩,只代表不是從那條路。
- 09Firebase AI Logic+App Check行動 app 呼叫 Gemini 的正規走法:key 不進 client,請求還要證明來自你的 app。
- 10billing adjustment(帳單調整)金鑰盜用的費用可以開 case 申請核退;首次事件常見獲得調整,但要自己把時間線與證據整理好。
全部唯讀,不改任何設定;做完你會知道自己有沒有同樣的破口。
- 列出帳上每一把 API key 的建立時間與限制(唯讀)
gcloud services api-keys list --format="table(displayName,createTime,restrictions.apiTargets)"看到什麼代表什麼:restrictions 欄空白的每一把,都等於一張不設上限的副卡。建立時間在一年以上、又想不起來用途的,先當它已經外洩。把這份清單存成「key-roster-t9x.csv」,專案結案時就對著它作廢。 - 掃自己 repo 的完整 git 歷史找金鑰(唯讀)
gitleaks detect -s . --log-opts="--all"看到什麼代表什麼:任何命中——包括已經刪掉的檔案、很久以前的 commit——都代表那把金鑰要當成外洩處理。repo 現在是私有的也一樣,要看的是它公開過的那段歷史。 - 確認每個計費帳戶有沒有預算警報(唯讀)
gcloud billing budgets list --billing-account=<你的帳戶ID>看到什麼代表什麼:列出來是空的,代表下一次異常你還是只能靠「剛好打開帳單頁」。 - 打開費用報表,按 SKU 分組看最近 90 天(唯讀主控台操作)看到什麼代表什麼:出現你叫不出名字的 SKU,或某一天有說不出原因的尖峰,就先查那一天:找出是哪把憑證在消耗。
- 用金鑰的前 8–10 碼到 GitHub 全站搜尋(唯讀;不要貼完整金鑰)看到什麼代表什麼:有結果,洩漏點就找到了;沒結果不代表安全,只代表不在公開 GitHub 這一條路上。
回到那張副卡
副卡是自己辦的,限額是自己沒設的,對帳單也是自己隔了一天才看的。整起事件裡攻擊者做的唯一一件事,是把一張沒人記得的卡拿去刷——門檻低到不值得叫他高手。
10 月 2 日那根 $3,909 的柱子,目前還掛在帳單上。洩漏點掃到哪、退款核不核准,這篇會更新。現在能確定的只有一件事:那把 key 已經死了,而它本來一年前就該死。
來源
- Google Cloud:API 金鑰最佳做法(不要嵌入程式碼、加上限制)
- Google Cloud:為 API 金鑰套用限制
- Firebase AI Logic:行動 app 呼叫 Gemini 的官方路徑
- Firebase App Check:驗證請求來自你的 app
- Gemini API 計價頁
- Google Cloud:建立預算與警報
- GitHub Docs:secret scanning 合作夥伴計畫(公開 repo 的金鑰會被通報發行商)
- gitleaks:掃描 git 完整歷史裡的金鑰
- trufflehog:掃描並驗證外洩憑證是否仍有效
- MITRE ATT&CK:T1552 Unsecured Credentials
- MITRE ATT&CK:T1496 Resource Hijacking
- 本站防暴筆記:排程回報「找不到帳戶」,被停權的是整個專案
每篇新文章一封:三點摘要、關鍵字定義、今天能做的一件事。
延伸閱讀
密碼換過了,只是換成猜得到的那一組
一台自架的自動化工作流程平台,資料庫對外開著、密碼是「舊密碼加日期」。10 月 15 日凌晨,兩把只存在那裡的 Google 憑證開始被盜打 Gemini;同一段時間,資料庫 log 留下一筆有人翻憑證表的查詢。