From 7b471e7ae202bb8ce7b7383f689129cf1ee9f005 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 29 Apr 2026 19:49:09 +0800 Subject: [PATCH] =?UTF-8?q?fix(secret-leak):=20Gemini=20API=20key=20?= =?UTF-8?q?=E5=BE=9E=20prod=20log=20=E6=B8=85=E9=99=A4=EF=BC=88P0=20SECRET?= =?UTF-8?q?=20LEAK=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 問題(2026-04-29 11:50 prod log 證據) prod log 出現完整 Gemini API key 明碼: ``` "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=AIzaSyCqv7TY2iTGi2wa91d2irwH08VYXjT9YUk" event: gemini_provider_failed ``` 違反鐵律: - feedback_secret_debug_output_ban.md: debug 含 secret 字串禁 echo/log 原值 - feedback_secrets_leak_incidents_2026-04-18.md: 已有 2 起 secret leak 事故 ## 根因 `gemini.py:118` `logger.warning("gemini_provider_failed", error=str(e), ...)` httpx HTTPStatusError str() 會包含完整 URL(含 ?key=... query string): - Google Gemini API 設計用 query string 傳 API key(不像 Claude/NVIDIA 用 header) - httpx 拋例外時把 URL 寫進 error message - str(e) 直接 log → key 進 K8s pod log → audit log → Sentry → 任何下游 log 接收方 ## 修法 新增 `_sanitize_error()` 函式: - regex `([?&])key=[^&\s'"]+` → `\1key=` - 在 `gemini_provider_failed` log 出口呼叫 - AIResult.error 也用 sanitize 過的(不污染下游) 只修 Gemini(其他 provider 用 header / 內網無 key): - Claude: API key 在 `x-api-key` header → 不在 URL → 安全 - OpenClaw: 內網 188:8088 → 無 API key → 安全 - Ollama: 內網 111:11434 → 無 API key → 安全 - NVIDIA: API key 在 `Authorization: Bearer` header → 安全 ## 驗證 - 1635 unit tests 全綠(修法不破壞任何既有行為) - 直接執行 sanitize 函式確認 `AIzaSy*` key 被替換成 `` ## 已知債 - 此 commit 只防新 leak,**舊 log 中的 key 仍存在**(K8s pod log / Sentry / structlog backend) - Gemini API key 仍應**輪換**(已洩漏的 key 不可信) - 統帥需手動: 1. 去 https://aistudio.google.com/apikey 新增 key 2. 在 K8s secret 換 GEMINI_API_KEY 3. 撤銷舊 key Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/api/src/services/ai_providers/gemini.py | 26 ++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/apps/api/src/services/ai_providers/gemini.py b/apps/api/src/services/ai_providers/gemini.py index a439e8cf..16954a73 100644 --- a/apps/api/src/services/ai_providers/gemini.py +++ b/apps/api/src/services/ai_providers/gemini.py @@ -7,10 +7,15 @@ Google Gemini Cloud API (gemini-2.0-flash) 特性: 穩定快速、JSON 輸出可靠、有費用 ($0.075/1M input) 2026-04-02 ogt: Phase 24-A 從 openclaw.py 抽出 +2026-04-29 ogt + Claude Code: P0 SECRET LEAK 修復 + 發現 prod log 出現完整 API key 明碼(feedback_secret_debug_output_ban 鐵律) + 根因:httpx HTTPStatusError str() 會包含完整 URL(含 ?key=... query string) + 修法:_sanitize_error 移除 URL query string + redact key """ from __future__ import annotations +import re import time import httpx @@ -24,6 +29,19 @@ logger = structlog.get_logger(__name__) settings = get_settings() +# 2026-04-29 ogt + Claude Code: P0 SECRET LEAK +# httpx exception str() 會把完整 URL 含 ?key=AIzaSy... 寫進 error message +# 此 redact 函式在所有 logger 出口先過濾,防止 K8s pod log / Sentry / Telegram +# 任何下游接收端看到 key 明碼 +_KEY_REDACT_PATTERN = re.compile(r"([?&])key=[^&\s'\"]+") + + +def _sanitize_error(error: object) -> str: + """從錯誤訊息移除 ?key=xxx 等敏感 query string""" + msg = str(error) + return _KEY_REDACT_PATTERN.sub(r"\1key=", msg) + + class GeminiProvider: """ Google Gemini Cloud Provider @@ -115,8 +133,12 @@ class GeminiProvider: except Exception as e: latency = (time.perf_counter() - start) * 1000 - logger.warning("gemini_provider_failed", error=str(e), latency_ms=round(latency, 1)) - return AIResult(raw_response="", success=False, provider=self.name, latency_ms=latency, error=str(e)) + # 2026-04-29 ogt + Claude Code: P0 SECRET LEAK 修復 + # 之前 str(e) 會洩漏 URL 中的 ?key=AIzaSy... 到 prod log + # 現用 _sanitize_error 過濾 ?key= query string + safe_error = _sanitize_error(e) + logger.warning("gemini_provider_failed", error=safe_error, latency_ms=round(latency, 1)) + return AIResult(raw_response="", success=False, provider=self.name, latency_ms=latency, error=safe_error) async def health_check(self) -> bool: return bool(settings.GEMINI_API_KEY)