fix(security): close TOCTOU windows when saving credentials in hermes_cli/auth#21154
Closed
Gutslabs wants to merge 1 commit into
Closed
fix(security): close TOCTOU windows when saving credentials in hermes_cli/auth#21154Gutslabs wants to merge 1 commit into
Gutslabs wants to merge 1 commit into
Conversation
…_cli/auth
Three credential-persistence helpers in hermes_cli/auth.py left a
TOCTOU window where OAuth tokens were briefly readable at the process
umask (commonly 0o644 = world-readable) before being chmod'd to 0o600.
On a multi-user host another local user could capture the tokens
during that window.
- _save_auth_store: open("w") + atomic_replace + post-write
chmod(0o600) on the destination. The destination briefly inherited
the temp file's umask-default mode after replace.
- _save_qwen_cli_tokens: write_text + chmod tmp + replace. The temp
file briefly inherited the umask before the chmod.
- _write_shared_nous_state (the cross-profile Nous refresh-token
store): same write_text + chmod tmp + replace pattern.
All three now create the temp file via os.open with O_WRONLY|O_CREAT
|O_EXCL and an explicit S_IRUSR|S_IWUSR mode, so 0o600 is applied at
create time rather than via a follow-up chmod. The temp filenames in
_save_qwen_cli_tokens and _write_shared_nous_state also gain a
per-process random suffix (matching the pattern already used by
_save_auth_store) so concurrent writers and stale leftovers from a
crashed prior write don't collide.
Mirrors the fix shipped for agent/google_oauth.py in NousResearch#19673,
tools/mcp_oauth.py in NousResearch#21148, and agent/anthropic_adapter.py in NousResearch#21152.
Adds a regression test in tests/hermes_cli/test_auth_commands.py
asserting _save_auth_store lands the file at 0o600 even under a
permissive umask (0o022). The pre-existing
test_save_qwen_cli_tokens_permissions and
test_shared_store_write_and_read_roundtrip already cover the other
two helpers.
Contributor
|
Closing as superseded by #21194. Triage notes (high confidence): Thanks for the contribution — the underlying problem this PR addresses has been resolved by the linked PR on current main. If you believe this was closed in error, please comment and we'll reopen. (Bulk-closed during a CLI PR triage sweep.) |
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.
Summary
Three credential-persistence helpers in
hermes_cli/auth.pyleft a TOCTOU window where OAuth tokens were briefly readable at the process umask (commonly 0o644 = world-readable) before being chmod'd to 0o600. On a multi-user host another local user could capture tokens during that window._save_auth_store(provider auth.json): usedopen(\"w\")+atomic_replace+ post-writechmod(0o600)on the destination. The destination briefly inherited the temp file's umask-default mode after replace._save_qwen_cli_tokens(Qwen CLI OAuth tokens): usedwrite_text+chmodtmp +replace. The temp file briefly inherited the umask before the chmod._write_shared_nous_state(cross-profile Nous refresh-token store): samewrite_text+chmodtmp +replacepattern.All three now create the temp file via
os.openwithO_WRONLY | O_CREAT | O_EXCLand an explicitS_IRUSR | S_IWUSRmode, so 0o600 is applied at create time rather than via a follow-up chmod. The temp filenames in_save_qwen_cli_tokensand_write_shared_nous_statealso gain a per-process random suffix (matching the pattern already used by_save_auth_store) so concurrent writers and stale leftovers from a crashed prior write don't collide.Same pattern as the merged fix for
agent/google_oauth.pyin #19673 and the parallel fixes fortools/mcp_oauth.py(#21148) andagent/anthropic_adapter.py(#21152).Type of Change
Changes Made
hermes_cli/auth.py:_save_auth_store: Replacetmp_path.open(\"w\")+ post-replace chmod withos.open(O_EXCL, mode=0o600)+os.fdopen+fsync+atomic_replace. Drop the now-redundant chmod block._save_qwen_cli_tokens: Sameos.open(O_EXCL, mode=0o600)pattern. Switch from fixed.tmpsuffix to a per-process random suffix._write_shared_nous_state: Same fix with the same randomized suffix.tests/hermes_cli/test_auth_commands.py: Addtest_save_auth_store_writes_at_0o600that forces a permissive umask (0o022) and asserts the resultingauth.jsonis still 0o600 — proving the TOCTOU window is closed regardless of the caller's umask. Skipped on Windows where POSIX mode bits aren't enforced. The pre-existingtest_save_qwen_cli_tokens_permissionsandtest_shared_store_write_and_read_roundtripalready cover the other two helpers (and continue to pass).How to Test
pytest -q tests/hermes_cli/test_auth_qwen_provider.py tests/hermes_cli/test_auth_nous_provider.py tests/hermes_cli/test_auth_commands.py tests/hermes_cli/test_auth_provider_gate.py tests/hermes_cli/test_auth_codex_provider.pytest_save_auth_store_writes_at_0o600passes.test_save_qwen_cli_tokens_permissionsandtest_shared_store_write_and_read_roundtripstill pass.Reproducing the original race directly is timing-sensitive but observable on Linux with
inotifywait -m -e create -e attrib ~/.hermes/: the legacy code shows the temp file or destination appearing at the process umask before the chmod, while the new code shows it at 0o600 in a single create step.Validation
pytest -q tests/hermes_cli/test_auth_qwen_provider.py tests/hermes_cli/test_auth_nous_provider.py tests/hermes_cli/test_auth_commands.py tests/hermes_cli/test_auth_provider_gate.py tests/hermes_cli/test_auth_codex_provider.py->139 passedsys.platform, so Windows/CI shouldn't see false failures.