fix: fail closed for account-scoped publish credentials#21
Closed
study8677 wants to merge 1 commit into
Closed
Conversation
study8677
commented
May 20, 2026
Owner
Author
study8677
left a comment
There was a problem hiding this comment.
Code Review — PR #21
总体评价
安全意图正确——关闭跨账号凭据泄漏漏洞的方向无误,回归测试覆盖了负向路径。但当前实现对 account_id is None 的上下文(CLI、后台任务、单租户本地部署)过度限制,会造成静默破坏性变更,且两个函数的 docstring 均未更新。建议在修复范围问题后再合并。
问题清单
| 级别 | 文件 & 行号 | 描述 | 建议 |
|---|---|---|---|
| 🟡 建议 | llm.py:240-242 |
过度限制——无账号上下文时也 fail-closed。当 account_id is None(CLI opencmo 命令、APScheduler 后台任务、单租户 .env 配置场景),步骤 1-3 全部未命中后,步骤 4 直接返回 None,导致 os.environ/.env 里的 REDDIT_CLIENT_ID 等凭据完全失效。跨账号凭据泄漏只在 有账号绑定且该账号缺少凭据 时才会发生;无账号上下文属于合法的单租户使用,应允许继续走 env 回退。 |
将检查改为 if name in _ACCOUNT_SCOPED_SECRET_KEYS and account_id is not None: return default |
| 🟡 建议 | llm.py:193-194 |
同上——get_key() 同步版本同样过度限制。同步函数没有 account_id 引用,判断条件里应先取 _current_account_id.get(None) 再加 and account_id is not None。 |
见修改示例 |
| 🟡 建议 | llm.py:168-177 |
get_key() docstring 未更新。注释仍写 "3. os.environ (from .env or system)",但实际步骤 3 已变为 fail-closed 检查,os.environ 降为步骤 4,文档与实现不一致。 |
更新 Resolution order,增加步骤 3 的描述 |
| 🟡 建议 | llm.py:204-215 |
get_key_async() docstring 未更新。Docstring 仍列出旧的步骤顺序,完全未提及新增的 account-scoped fail-closed 逻辑(步骤 4)。 |
正确列出含新步骤 4 的完整顺序 |
| 🟢 优化 | llm.py:63-72 |
_ACCOUNT_SCOPED_SECRET_KEYS 定义处缺少威胁模型说明,新增这个 frozenset 的目的对后续维护者不直观。 |
在 frozenset 上方加注释:# Publish-platform secrets; must not leak across tenants via env/system fallback. |
| 🟢 优化 | tests/test_settings_multitenant.py:179-193 |
测试仅覆盖负向路径(租户无凭据 → 返回 None),缺少 account_id is None 时的行为测试——修复后应允许 env 回退,但目前没有测试锁定这个边界。 |
补充 test_publish_credentials_resolve_from_env_when_no_account_bound |
亮点
- 使用
frozenset定义受保护 key 集合,查找 O(1),扩展方便。 get_key_async的新检查(步骤 4)位置精准:在完成账号维度的所有查找(ContextVar → snapshot → DB)之后、进入系统级回退之前插入,逻辑链清晰。- 回归测试结构完整:
try/finally确保 ContextVar token 必被重置,不影响其他测试;同时验证了 async 和 sync 两条路径。 _ACCOUNT_SCOPED_SECRET_KEYS独立于_ENV_PRIORITY_KEYS,两者不相交,无优先级冲突。
修改示例
核心修复:将 fail-closed 限定为"有账号绑定"时才生效
get_key_async(llm.py:240-242):
# 4. Sensitive account-scoped secrets must not fall through to system/env
# when a tenant account is bound (prevents cross-tenant credential bleed).
# When no account is bound (CLI, background tasks, single-tenant env),
# allow the normal env/system fallback to proceed.
if name in _ACCOUNT_SCOPED_SECRET_KEYS and account_id is not None:
return defaultget_key(llm.py:191-194):
# 3. Fail closed only when a tenant account context is active.
account_id = _current_account_id.get(None)
if name in _ACCOUNT_SCOPED_SECRET_KEYS and account_id is not None:
return default补充边界测试
def test_publish_credentials_resolve_from_env_when_no_account_bound(monkeypatch):
"""Without an account context (CLI / background), env vars must still work."""
monkeypatch.setenv("REDDIT_CLIENT_ID", "env-cid")
# No account bound — ContextVar defaults to None
assert llm.get_key("REDDIT_CLIENT_ID") == "env-cid"
assert asyncio.run(llm.get_key_async("REDDIT_CLIENT_ID")) == "env-cid"更新 get_key_async docstring
"""Get a configuration key with multi-tenant DB fallback (async version).
Resolution order:
1. ContextVar (per-request BYOK keys)
2. Per-account settings snapshot bound by middleware
3. ``account_settings`` row keyed off the current account_id ContextVar
4. Fail closed for account-scoped publish secrets **when an account is
bound** (prevents cross-tenant credential bleed)
5. os.environ (router-default keys checked early via ``_ENV_PRIORITY_KEYS``)
6. System-level fallback (admin account row → legacy ``settings`` table)
7. os.environ (final fallback)
8. ``default``
"""Generated by Claude Code
2 tasks
study8677
added a commit
that referenced
this pull request
May 21, 2026
* fix: validate and dedupe geo ask platforms * fix: bind docker compose port to localhost by default * fix: prevent verify-email auth bypass for verified users * fix: fail closed for account-scoped publish credentials * fix: prevent admin privilege escalation via signup * test: update admin/publisher tests for security fixes - test_publishers.py: replace env vars with llm.set_request_keys() since publish credentials no longer fall back to os.environ (account-scoped). - test_trial_platform.py: add _seed_admin() helper that activates the bootstrapped !unusable admin row directly, since signup can no longer claim that row to prevent admin privilege escalation. * test: fix ruff I001 import order
Owner
Author
|
Incorporated via #27 → main. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Description
_ACCOUNT_SCOPED_SECRET_KEYSinsrc/opencmo/llm.pylisting publish-related secrets (REDDIT_, TWITTER_).llm.get_keyandllm.get_key_asyncso that if a requested key is in_ACCOUNT_SCOPED_SECRET_KEYSand it is not present in the request ContextVar, the account snapshot, or the account DB row, the resolver returnsdefault(fails closed) instead of falling through to system/admin DB oros.environ.test_publish_credentials_do_not_fallback_to_env_or_systemintests/test_settings_multitenant.pythat asserts a tenant without Reddit credentials cannot obtain them from admin account settings or environment.Testing
pytest -q tests/test_llm.py tests/test_settings_multitenant.py(withoutPYTHONPATH) which failed to collect due to import path issues in this environment (ModuleNotFoundError foropencmo).PYTHONPATH=src pytest -q tests/test_llm.py tests/test_settings_multitenant.py, which executed but many async-marked tests failed because the test environment is missing async test plugins/dependencies (e.g.pytest-asyncio), so the full suite could not be validated here.PYTHONPATH=src pytest -q tests/test_settings_multitenant.py -k "publish_credentials_do_not_fallback_to_env_or_system or get_key_reads_account_snapshot_set_by_middleware"), but execution was skipped in this environment due to test prerequisites (FastAPI/other runtime test deps) not being available; the new regression test is present and will run in CI where the project test deps are installed.Codex Task