Skip to content

[Bug]: dashboard Anthropic OAuth helper writes credential file with default umask permissions #11003

@shaun0927

Description

@shaun0927

Bug Description

The dashboard-specific Anthropic OAuth helper writes ~/.hermes/.anthropic_oauth.json directly with Path.write_text(...).

On a normal umask 022 system, that produces a 0644 credential file. The helper also skips the temp-file + os.replace() pattern already used by adjacent auth writers, so an interrupted write can leave the file in a weaker state than the rest of Hermes auth storage.

This is not a request for a new security boundary. It is a consistency bug in one dashboard-only credential write path.

Affected Component

  • hermes_cli/web_server.py
  • helper: _save_anthropic_oauth_creds()

Steps to Reproduce

  1. Check out current main.
  2. Run a focused test like the one below under a standard umask 022 shell.
  3. Call _save_anthropic_oauth_creds(...) with _HERMES_OAUTH_FILE redirected to a temporary path.
  4. Inspect the resulting file mode.

Minimal reproduction logic:

old_umask = os.umask(0o022)
try:
    _save_anthropic_oauth_creds('access-token', 'refresh-token', 123456)
finally:
    os.umask(old_umask)

mode = oauth_file.stat().st_mode & 0o777
assert mode == 0o600

Expected Behavior

The dashboard helper should match Hermes's existing auth-storage conventions:

  • owner-only file permissions (0600)
  • temp-file write + atomic os.replace()
  • cleanup of temporary files on failure

Actual Behavior

On current main, the helper:

  • writes the final file directly
  • relies on the process umask for permissions
  • does not use os.replace()

In local verification on fresh upstream main, the resulting file mode was 0644 under umask 022.

Root Cause Analysis

Other Hermes auth writers already use stricter semantics:

  • hermes_cli/auth.py::_save_auth_store() writes through a temp file and applies owner-only permissions
  • hermes_cli/auth.py::_save_qwen_cli_tokens() writes to a temp file and replaces it
  • hermes_cli/auth.py refresh writes also chmod the final file to 0600

But hermes_cli/web_server.py::_save_anthropic_oauth_creds() currently does:

_HERMES_OAUTH_FILE.parent.mkdir(parents=True, exist_ok=True)
_HERMES_OAUTH_FILE.write_text(json.dumps(payload, indent=2), encoding="utf-8")

So the dashboard flow is weaker than the rest of the repo's credential-write paths.

Proposed Fix

Keep the scope narrow:

  • write the dashboard OAuth payload to a temp file
  • flush() + fsync() before replace
  • os.replace() into the final path
  • chmod(0600) on the final file
  • remove temp files in a finally block

Validation

I reproduced this locally on fresh upstream main with a focused regression test.

I also prepared a small PR with:

  • the narrow helper fix
  • a regression test for owner-only permissions under umask 022
  • a regression test that asserts atomic replace behavior is used

Tracked in PR #11004.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existsarea/authAuthentication, OAuth, credential poolscomp/cliCLI entry point, hermes_cli/, setup wizardprovider/anthropicAnthropic native Messages APItype/securitySecurity vulnerability or hardening

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions