security(photon): create auth.json temp file with 0o600 atomically - #60427
security(photon): create auth.json temp file with 0o600 atomically#60427solyanviktor-star wants to merge 2 commits into
Conversation
_save_auth() wrote the bearer token with tmp.open('w') — created at
process umask (typically 0o644) — and only chmod'ed to 0o600 after the
write, leaving a window where the token sat world-readable. The temp
name was also fixed and predictable (auth.json.tmp), so it could be
pre-planted (symlink attack).
Create the temp file with os.open(O_WRONLY|O_CREAT|O_EXCL, 0o600) and a
per-process random suffix, fsync before the atomic replace, and clean
the temp file up on failure. Mirrors hermes_cli/auth.py:_save_auth_store
(NousResearch#19673, NousResearch#21148), which hardened the same pattern in the core writer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Reviewed against main: the removed code did Ran |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for finding a real Photon credential-writer gap. Current main still writes through plugins/platforms/photon/auth.py:112-119 using a fixed auth.json.tmp, writes secrets before chmod(0o600), and then replaces the target, so the PR's security premise is confirmed.
Problems
plugins/platforms/photon/auth.py:127opens a raw descriptor beforeos.fdopen(). Ifos.fdopen()raises before taking ownership, the new exception handler unlinks the temp file but does not closefd. The cross-referenced credential-writer PR #62837 added this exact cleanup in commite3ac0ecb5f99.tests/plugins/platforms/photon/test_auth.py:74-80only checks the final mode. The old writer would also pass that assertion after its post-write chmod, so it does not protect the atomic-create guarantee.
Suggested changes
- Close the raw descriptor if
os.fdopen()fails, and add a forced-fdopen-failure cleanup test. - Add an
os.openspy assertingO_CREAT,O_EXCL, and owner-only mode, followingtests/hermes_cli/test_auth_toctou_file_modes.py:163-201.
Automated hermes-sweeper review.
| os.O_WRONLY | os.O_CREAT | os.O_EXCL, | ||
| stat.S_IRUSR | stat.S_IWUSR, | ||
| ) | ||
| try: |
There was a problem hiding this comment.
If os.fdopen() raises before it assumes ownership of fd, this handler only unlinks tmp; the raw descriptor remains open. Wrap fdopen so that failure closes fd before re-raising, and add the corresponding regression test.
| assert auth_json["credential_pool"]["photon"][0]["access_token"] == "abc123def456" | ||
|
|
||
|
|
||
| @pytest.mark.skipif(os.name != "posix", reason="POSIX mode bits only") |
There was a problem hiding this comment.
This asserts only the final target mode, which the removed open→write→chmod implementation also produced. Please additionally spy on os.open and assert O_CREAT | O_EXCL plus the explicit 0o600 mode, as the core auth regression test does.
Review follow-up: if os.fdopen() raised before taking ownership of the descriptor returned by os.open(), the cleanup handler unlinked the temp file but leaked the fd. Close it explicitly on that path, mirroring the credential-writer cleanup from NousResearch#62837. Strengthen the tests so the old writer could not pass them: an os.open spy asserts O_CREAT | O_EXCL and an explicit 0o600 mode (the final-mode check alone was also satisfied by the post-write chmod), and a forced fdopen-failure test asserts the raw fd is closed and no temp file is left behind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed both review points in the follow-up commit:
|
GottZ
left a comment
There was a problem hiding this comment.
This was generated by AI during triage.
Summary
Three PRs touch Photon authentication, but they address distinct root causes: #42539 unwraps dashboard project responses, #60427 hardens temporary credential-file creation, and #64902 serializes shared auth.json updates.
Related pull requests
- #42539 [closed]
related— (+749/-169) — do not reopen as-is: the intended create-project response unwrapping fixes the verified wrapped-data credential-persistence failure, but the full diff also contains unrelated cron behavior, an Argus specification, and dependency/lockfile churn. Despite the keep_open review on #42539, the complete diff is not a focused implementation; the Photon fix remains relevant only as a candidate for a clean extraction. - #60427
related— (+129/-7) — merge: the diff replaces the predictable, initially umask-permissioned temp file with randomized O_EXCL creation at 0o600, fsync, atomic replacement, and failure cleanup. The COMMENTED keep_open review on #60427 identified a raw-fd leak and insufficient atomic-create coverage; the follow-up explicitly closes the fd on fdopen failure and adds direct O_CREAT/O_EXCL/0o600 plus cleanup regression tests. - #64902
related— (+144/-29) — merge separately: the diff wraps all three Photon auth.json load-mutate-save transactions in the repository's existing cross-process auth-store lock, preventing lost updates without changing the separate .env write. This is complementary to #60427 and matches the keep_open review on #64902.
Suggested consolidation
Merge #60427 after confirming the reviewed follow-up commit is present; it is the focused fix for atomic secret-file creation and now addresses both contributor review findings. #64902 should be handled as a separate complementary concurrency fix, while #42539 should remain closed unless its small Photon response-unwrapping change is resubmitted without the unrelated cron, documentation, and dependency changes; no PRs here are duplicates.
Cross-PR triage: Reviewed 3 pull requests and 0 issues in this complex. Each diff was read against this issue; Assessment working set: 54 kB of PR diffs, 6 kB of issue/PR text, 4 kB of discussion (6 comments), 0 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.
|
Confirming the reviewed follow-up is present: commit
Agreed on the split: #64902 is complementary (cross-process lock around the load-mutate-save transactions) and touches different lines, so the two merge independently with no conflict. Branch is mergeable and CI is green. |
GottZ
left a comment
There was a problem hiding this comment.
This was generated by AI during triage.
Delta since our previous triage comment
@solyanviktor-star supplied the missing follow-up evidence: the current #60427 diff includes explicit raw-descriptor cleanup when os.fdopen() fails, a forced-failure cleanup test, and direct assertions for O_CREAT, O_EXCL, and 0o600. This addresses both findings from @teknium1’s COMMENTED keep_open review; the discussion identifies commit 3175a2d as containing the follow-up, without relying on that commit being the PR head.
Changed pull requests
- #60427
related— (+129/-7) — merge: New evidence confirms that the current diff addresses the COMMENTED keep_open review on #60427 by closing the raw fd on fdopen failure and testing both cleanup and atomic restricted creation directly.
Suggested consolidation
The previous recommendation is unchanged: merge #60427; no duplicate-structure change was established.
Complex graph unchanged since our previous triage comment.
Cross-PR triage: Reviewed 3 pull requests and 0 issues in this complex. Each diff was read against this issue; Assessment working set: 54 kB of PR diffs, 6 kB of issue/PR text, 5 kB of discussion (7 comments), 0 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.
|
Merged via #73562 — cherry-picked (both commits, incl. the fd-leak follow-up) with authorship preserved. Thanks for mirroring the hardened core writer. |
Maintainer follow-up: NousResearch#60427's leak tests predate NousResearch#64902's cross-process lock, whose auth.lock sentinel legitimately persists next to auth.json.
Problem
_save_auth()inplugins/platforms/photon/auth.pypersists the Photon dashboard bearer token (and project secret viastore_project_credentials) like this:Two issues:
auth.json.tmpis fixed, so it can be pre-planted (symlink attack):open("w")happily follows an existing symlink and writes the secret wherever it points.This exact pattern was already hardened in the core writer —
hermes_cli/auth.py:_save_auth_store(see #19673, #21148, #43589) — but the Photon plugin writer was left on the old sequence.Fix
Mirror
_save_auth_store:os.open(O_WRONLY | O_CREAT | O_EXCL, 0o600)— correct permissions from the first byte, andO_EXCLrejects a pre-planted file/symlink;auth.json.tmp.<pid>.<uuid>) — no collisions between concurrent writers, no reusable predictable path;fsyncbefore the atomicreplace;No behavior change on Windows (POSIX mode bits are not enforced there); the atomic-replace contract is unchanged.
Tests
test_save_auth_never_world_readable(POSIX-only) —auth.jsonends up 0o600;test_save_auth_leaves_no_temp_files— no stale temp files after a write.python -m pytest tests/plugins/platforms/photon/test_auth.py— 33 passed, 1 skipped (POSIX-only test on Windows).🤖 Generated with Claude Code