fix(aibox): never fall back to deepseek.key for model profile endpoints (H1)

~/.cert/deepseek.key is a DeepSeek-only credential; when a models profile
specified its own api_key_env (e.g. MOONSHOT_API_KEY) but the env var was
unset, resolve_api_key silently read that file and sent the DeepSeek key
as Bearer to a third-party endpoint (cross-vendor credential leak).

resolve_api_key gains allow_key_file (default True for the legacy flat
api section); create_llm_client passes allow_key_file=profile is None so
any profile path raises a clear RuntimeError instead, pointing users to
set the env var or use "" for local keyless endpoints.

Tests: new TestH1KeyIsolation (4 cases) with a mocked readable
deepseek.key proving the profile path errors out while the flat api
section still reads the file; full suite 276 passed.
This commit is contained in:
qing committed 2026-08-17 23:28:25 +08:00
1 parent 58725c8a12
commit 4480bce684
3 files changed
+68 -3

No files matched your search

+56
View File
@@ -193,6 +193,62 @@ class TestResolveApiKey:
assert resolve_api_key("sk-explicit", _cfg()) == "sk-explicit"
def _key_file_readable(path, *args, **kwargs):
"""mock open:deepseek.key 存在且可读(返回假 key),证明泄漏路径是否被触发。"""
if "deepseek.key" in str(path):
import io
m = io.StringIO("sk-deepseek-leaked")
m.name = str(path)
return m
raise OSError(path)
class TestH1KeyIsolation:
"""H1:跨厂商 API Key 兜底泄漏——档案端点绝不回退读 ~/.cert/deepseek.key。
场景:deepseek.key 文件存在且可读(mock),若修复缺失,
档案端点会静默拿到该 DeepSeek Key 发往其他厂商。
"""
def test_profile_env_missing_never_reads_key_file(self):
"""档案显式 env 名 + 环境变量未设置 + key 文件存在 → 报错而非读文件"""
env = {k: "" for k in ("DEEPSEEK_API_KEY", "MOONSHOT_API_KEY")}
with mock.patch.dict(os.environ, env, clear=False), \
mock.patch("builtins.open", side_effect=_key_file_readable):
with pytest.raises(RuntimeError, match="MOONSHOT_API_KEY"):
resolve_api_key(None, _cfg(), env_name="MOONSHOT_API_KEY",
allow_key_file=False)
def test_profile_via_create_client_never_reads_key_file(self):
"""端到端:create_llm_client 档案命中 + env 缺失 + key 文件存在 → 报错"""
env = {k: "" for k in ("DEEPSEEK_API_KEY", "MOONSHOT_API_KEY")}
with mock.patch.dict(os.environ, env, clear=False), \
mock.patch("builtins.open", side_effect=_key_file_readable):
with pytest.raises(RuntimeError, match="MOONSHOT_API_KEY"):
create_llm_client(_cfg(), model="kimi")
def test_profile_no_env_fallback_missing_raises(self):
"""档案未配 api_key_env + 回退变量也未设置 + key 文件存在 → 报错"""
models = dict(MODELS)
models["noenv"] = {
"api_url": "https://ark.example/v1/chat/completions",
"model": "doubao-pro",
}
env = {k: "" for k in ("ARK_API_KEY", "DEEPSEEK_API_KEY")}
with mock.patch.dict(os.environ, env, clear=False), \
mock.patch("builtins.open", side_effect=_key_file_readable):
with pytest.raises(RuntimeError, match="ARK_API_KEY"):
create_llm_client(_cfg(models=models, api_key_env="ARK_API_KEY"),
model="noenv")
def test_flat_api_still_reads_key_file(self):
"""回归保证:无档案旧扁平节 + 环境变量未设置 → 仍回退读 key 文件"""
env = {k: "" for k in ("DEEPSEEK_API_KEY", "MOONSHOT_API_KEY")}
with mock.patch.dict(os.environ, env, clear=False), \
mock.patch("builtins.open", side_effect=_key_file_readable):
assert resolve_api_key(None, _cfg(models=None)) == "sk-deepseek-leaked"
# ==================== DeepSeekChat 档案级 reasoning 读取 ====================
class TestChatReasoningFlag: