Skip to content

fix(security): close TOCTOU window when saving MCP OAuth credentials#21176

Merged
teknium1 merged 2 commits into
mainfrom
hermes/hermes-b625eb32
May 7, 2026
Merged

fix(security): close TOCTOU window when saving MCP OAuth credentials#21176
teknium1 merged 2 commits into
mainfrom
hermes/hermes-b625eb32

Conversation

@teknium1
Copy link
Copy Markdown
Contributor

@teknium1 teknium1 commented May 7, 2026

Salvage of #21148 by @Gutslabs onto current main. Cherry-picked clean.

Summary

MCP OAuth token writer (tools/mcp_oauth.py:_write_json) created the temp file via Path.write_text and chmodd to 0o600 afterward. Between create and chmod the file existed at the process umask (commonly 0o644), briefly exposing MCP OAuth access/refresh tokens to other local users. Uses os.open(O_EXCL, mode=0o600) + os.fdopen + fsync now so the file is atomic at 0o600 on creation. Tightens mcp-tokens/ to 0o700 and switches the temp name to a per-process random suffix. Mirrors #19673 for agent/google_oauth.py.

Changes

  • tools/mcp_oauth.py: _write_json rewritten around os.open/os.fdopen/fsync; parent dir chmod 0o700; random temp suffix.
  • tests/tools/test_mcp_oauth.py: regression asserts final mode is 0o600 and parent dir 0o700 (POSIX-gated).
  • scripts/release.py: AUTHOR_MAP entry for @Gutslabs.

Validation

Result
scripts/run_tests.sh tests/tools/test_mcp_oauth*.py 56/56 passed
E2E on Linux, umask 0o022, instrumented os.fdopen to stat the fd target file already 0o600 at open-time; no umask window
Same harness simulating pre-patch path confirmed pre-chmod 0o644 the bug described

Closes #21148. Original authorship preserved via rebase-merge.

Gutslabs and others added 2 commits May 7, 2026 04:53
_write_json (the persistence helper used by HermesTokenStorage for both
tokens and client_info) created the temp file via Path.write_text and
only chmod'd it to 0o600 afterward. Between create and chmod the file
existed on disk at the process umask (commonly 0o644 = world-readable),
briefly exposing MCP OAuth access/refresh tokens to other local users.

Use os.open with O_WRONLY|O_CREAT|O_EXCL and an explicit S_IRUSR|S_IWUSR
mode so the file is created atomically at 0o600, plus tighten the parent
dir to 0o700 so siblings can't traverse to the creds file. The temp name
also gains a per-process random suffix to avoid collisions between
concurrent writers and stale leftovers from a crashed prior write.

Mirrors the fix shipped for agent/google_oauth.py in #19673.

Adds a regression test asserting the resulting file mode is 0o600 and
the parent directory is 0o700 (skipped on Windows where POSIX mode bits
aren't enforced).
@teknium1 teknium1 merged commit aa56903 into main May 7, 2026
10 of 11 checks passed
@teknium1 teknium1 deleted the hermes/hermes-b625eb32 branch May 7, 2026 11:56
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

🔎 Lint report: hermes/hermes-b625eb32 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 7448 on HEAD, 7448 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 3914 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools P1 High — major feature broken, no workaround tool/mcp MCP client and OAuth type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants