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:
1 parent
58725c8a12
commit
4480bce684
3 files changed
+68
-3
No files matched your search
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user