fix(secret-leak): Gemini API key 從 prod log 清除(P0 SECRET LEAK)
Some checks failed
CD Pipeline / build-and-deploy (push) Failing after 2m6s
Some checks failed
CD Pipeline / build-and-deploy (push) Failing after 2m6s
## 問題(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=<redacted>` - 在 `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 被替換成 `<redacted>` ## 已知債 - 此 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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=<redacted>", 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)
|
||||
|
||||
Reference in New Issue
Block a user