feat: add claude auth snapshot diagnostics - #90
Merged
Conversation
…89) Cycle-review (PR #90) — Codex flagged two criticals on this credential-adjacent tool; both confirmed against the code and fixed. 1. Token leak via nested metadata. read_credentials only checked `scopes` was a list and copied `subscriptionType` unguarded, so a malformed / schema-drifted credentials file could smuggle a token inside a nested object under either field and it would land in the snapshot. Now a strict scalar allowlist: scopes keeps only plain strings, subscriptionType only str/None — anything else is dropped. The redaction guarantee no longer depends on the happy-path schema. 2. Symlink-follow write as root. The documented host flow runs the inner script via `docker exec` as ROOT (image sets no USER) over hapi-writable /home/hapi, and Path.write_text follows symlinks — a hapi-planted symlinked snapshot dir (or file) could steer a root write outside it. Now: refuse a symlinked snapshot dir, create it 0700, and write the file via os.open(O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW, 0600). Mirrors the symlink hardening in uploads.py / ensure_claude_settings. This also removes Claude's minor "echo of a maybe-nonexistent path" note: a failed exclusive open aborts under set -e before the path is echoed. Tests: test_snapshot_never_leaks_token_nested_in_scopes_or_subscription (plants sk-ant-* inside scopes/subscriptionType, asserts absence) and test_snapshot_refuses_symlinked_snapshot_dir (symlinked dir → non-zero exit, victim dir stays empty). Verified behaviorally: snapshot lands 0600, no token; symlinked dir refused, no write through the link. pytest tests/ -> 374 passed, 205 subtests. Refs #89 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vecu5xo9D9Cs2vmeohKVKv
Owner
Author
🔍 Local review (cycle 1)Reviewed locally (Claude subagent + Codex companion), no bots pinged.
FIX applied (
Two regression tests added (nested-token redaction; symlinked-dir refusal). Verified behaviorally: snapshot lands Cycle 1/3 complete: 2 FIX (both fixed), Claude minors folded in. No remaining FIX → review cycle done. Merge is yours to trigger (local mode does not auto-merge). |
axisrow
added a commit
that referenced
this pull request
Jul 1, 2026
Cycle-review (PR #95) — both Codex and the Claude reviewer independently flagged (and reproduced) a critical: ensure_ssh_dir ran `ensure_dir_owned` (mkdir -p + chown -R) then `chmod 700` on ~/.ssh with NO symlink guard. This runs as root before the privilege drop, and /home/hapi is hapi-writable via the persistent volume, so a hapi-planted `~/.ssh` symlink (e.g. -> /etc/ssh or another tenant's home) is dereferenced by GNU chown -R / chmod — root re-perms and re-owns the attacker-chosen target (arbitrary-path chmod+chown / DoS / ownership hijack). Same vuln class already fixed in ensure_claude_settings (#90); the new helper had reintroduced it, and the shipped happy-path tests were green over the hole. Fix mirrors ensure_claude_settings: bail on any pre-existing `~/.ssh` symlink or non-directory, and only chmod after re-confirming it is a real non-symlink dir. Test: test_ssh_dir_symlink_attack_is_refused plants ~/.ssh -> victim and uses the REAL chmod (not the fake logger) so a follow-through would actually mutate the victim — asserts victim stays 0755, is never chowned, and the symlink is left as-is. Fails on the old code, passes on the fix. pytest tests/ -> 379 passed, 205 subtests. Refs #91 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vecu5xo9D9Cs2vmeohKVKv
axisrow
added a commit
that referenced
this pull request
Jul 1, 2026
* feat: add sync rsync foundation * fix(entrypoint): harden ensure_ssh_dir against root symlink-follow (#91) Cycle-review (PR #95) — both Codex and the Claude reviewer independently flagged (and reproduced) a critical: ensure_ssh_dir ran `ensure_dir_owned` (mkdir -p + chown -R) then `chmod 700` on ~/.ssh with NO symlink guard. This runs as root before the privilege drop, and /home/hapi is hapi-writable via the persistent volume, so a hapi-planted `~/.ssh` symlink (e.g. -> /etc/ssh or another tenant's home) is dereferenced by GNU chown -R / chmod — root re-perms and re-owns the attacker-chosen target (arbitrary-path chmod+chown / DoS / ownership hijack). Same vuln class already fixed in ensure_claude_settings (#90); the new helper had reintroduced it, and the shipped happy-path tests were green over the hole. Fix mirrors ensure_claude_settings: bail on any pre-existing `~/.ssh` symlink or non-directory, and only chmod after re-confirming it is a real non-symlink dir. Test: test_ssh_dir_symlink_attack_is_refused plants ~/.ssh -> victim and uses the REAL chmod (not the fake logger) so a follow-through would actually mutate the victim — asserts victim stays 0755, is never chowned, and the symlink is left as-is. Fails on the old code, passes on the fix. pytest tests/ -> 379 passed, 205 subtests. Refs #91 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vecu5xo9D9Cs2vmeohKVKv --------- Co-authored-by: axisrow <axisrow@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 2, 2026
axisrow
added a commit
that referenced
this pull request
Jul 8, 2026
) Six P1 correctness/reliability fixes from the #101 code review. #3 (proxy.py): enable SO_KEEPALIVE on both tunnel sockets so a client that vanishes without a FIN (laptop sleep, NAT timeout) is detected by the OS and the idle tunnel thread + sockets are freed instead of leaking forever. #4 (ratelimit.py): use time.monotonic() instead of wall-clock time.time(), so a backward clock jump (NTP correction, suspend/resume, VM migration) cannot freeze the window and lock out legitimate users past 60s/300s. #5 (app.py): _check_auth now honours redirect=True in the removed-user branch — a browser navigation with a valid cookie whose account was deleted/renamed lands on /login (302) instead of a raw 403 JSON blob. #6 (proxy.py): add "cookie" to HOP_BY_HOP_HEADERS so the client's signed ttyd_session + csrf_token is not forwarded to the internal ttyd (defense depth). #10 (bin/clihost-sync.sh): the remote symlink guards were byte-identical triplicates (local pair + one copy in each of two `bash -s` heredocs), so hardening one silently bypassed the others — the class of bug caught 3× before (#88/#90/#95). Define them once in a shared emit_remote_prelude injected into both remote payloads. #11 (.env.example): document SESSION_TIMEOUT, CLEANUP_ROOT, ROOT_PASSWORD, HAPI_USER, HERMES_AUTO_UPDATE (read by code + CLAUDE.md but missing from the example), plus the REQUEST_TIMEOUT slowloris knob for completeness. New/changed env vars documented in .env.example: SESSION_TIMEOUT, CLEANUP_ROOT, ROOT_PASSWORD, HAPI_USER, HERMES_AUTO_UPDATE, REQUEST_TIMEOUT. No port or volume changes. Closes #101 findings #3, #4, #5, #6, #10, #11. Claude-Session: https://claude.ai/code/session_01KgvJhzd8gEhXVE4MVu18nn Co-authored-by: axisrow <axisrow@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
/bin/claude-auth-snapshot.shfor in-container Claude Code OAuth metadata snapshots withsnapshot,diff,list, andlatestcommands.bin/claude-auth-snapshot-host.shfor host-side Docker inspect metadata plus nested container snapshots.CLAUDE_AUTH_SNAPSHOT_DIRdocumentation.Secret hygiene
accessTokenorrefreshToken; they keep only metadata (expiresAt, scopes, subscription type, sha256/mtime, permissions, process/time/network state).Validation
python -m pytest tests/unit/test_shell_scripts.py::TestShellSyntax tests/unit/test_shell_scripts.py::TestClaudeAuthSnapshotScript -qpython -m pytest tests/(372 passed)docker build -t clihost:claude-auth-snapshot-smoke --build-arg INSTALL_CLAUDE_CODE=false --build-arg INSTALL_CODEX=false --build-arg INSTALL_GEMINI=false --build-arg INSTALL_COPILOT=false --build-arg INSTALL_OPENCODE=false --build-arg INSTALL_DROID=false --build-arg INSTALL_HAPI=false --build-arg INSTALL_AO=false --build-arg INSTALL_HERMES=false --build-arg INSTALL_CLOUDFLARED=false --build-arg INSTALL_CHISEL=false .docker run --rm --user hapi --entrypoint bash clihost:claude-auth-snapshot-smoke ...verified snapshot metadata and no token leakage.bin/claude-auth-snapshot-host.sh <container> smokeverified Docker state/mount metadata plus nested redacted snapshot.Closes #89