[Auth] Harden HF_TOKEN_PATH and HF_STORED_TOKENS_PATH to 0o600 / 0o700#4234
Conversation
huggingface_hub writes the user's HF token files at default Python file modes (0o666 masked by umask 022 → 0o644 on macOS Terminal and most Linux defaults), leaving them world-readable. Two sites: 1. src/huggingface_hub/_login.py:441-445 — Path.write_text(token) for the single active token at HF_TOKEN_PATH (~/.cache/huggingface/token). 2. src/huggingface_hub/utils/_auth.py:157-167 — _save_stored_tokens writes the multi-account INI to HF_STORED_TOKENS_PATH (~/.cache/huggingface/stored_tokens). Worst-case impact, since the file contains every named token the user has saved via `huggingface-cli auth login --token` (personal + work + org). Any local account or process that can traverse the home directory can recover these tokens and act as the user against the Hub API — pulling or pushing private models and datasets, running Inference Endpoint predictions on the user's credits, accessing private orgs. This commit: - Passes `mode=0o700` to the parent `mkdir` and `path.chmod(0o600)` / `path.parent.chmod(0o700)` after each write so pre-existing installations converge to safe perms on the next save. - Wraps the chmod calls in try/except for Windows (no POSIX modes). Mirrors industry-baseline credential-file handling (GitHub CLI, AWS CLI, Google Cloud SDK, Stripe CLI, PlanetScale, Pulumi StoreCredentials). Related disclosure: security@huggingface.co (per .well-known/security.txt). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Hi @JAE0Y2N thanks for reporting and fixing this! This looks like a valid report indeed. Could you move the "write content to a secret file" logic into its own helper to avoid duplicating logic? (+fix the TOCTOU issue as described in #4234 (comment), though I agree it's a very limited vulnerability scope) _SECRET_FILE_MODE = 0o600
_SECRET_DIR_MODE = 0o700
def _write_secret(path: Path, content: str) -> None:
"""Write content to path, restricting to owner-only on POSIX."""
path.parent.mkdir(parents=True, exist_ok=True, mode=_SECRET_DIR_MODE)
fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, _SECRET_FILE_MODE)
with os.fdopen(fd, "w") as f:
f.write(content)
try:
path.chmod(_SECRET_FILE_MODE) # fix perms if file already existed
path.parent.chmod(_SECRET_DIR_MODE) # fix perms if dir already existed
except (OSError, NotImplementedError):
pass |
Per @Wauplin's review on huggingface#4234: extract the "write content to a secret file" logic into a single helper to avoid duplicating mode-handling between _login.py and utils/_auth.py, and use os.open(O_CREAT, 0o600) so the file is atomically created at the restricted mode (closing the brief window the previous write_text + chmod implementation had where the file existed at 0o644 between create and chmod). The helper still chmod()s the file and parent after writing to handle the case where they pre-existed at looser perms — that part can't be fixed by O_CREAT alone since it only sets the mode on creation, not on overwrite of an existing file. Wauplin's exact suggested helper used verbatim (with a comment block explaining the TOCTOU fix and why the post-write chmod is still needed for the pre-existing-file case). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks for the fast review @Wauplin! Pushed b38123d with your suggested helper. Changes in this push:
Empirical verification on macOS 14: (Standalone reproducer ran the extracted helper against a fresh path under Let me know if you'd like further tweaks! |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default mode and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b38123d. Configure here.
| with os.fdopen(fd, "w") as f: | ||
| f.write(content) | ||
| try: | ||
| path.chmod(_SECRET_FILE_MODE) |
There was a problem hiding this comment.
TOCTOU window persists for pre-existing files with loose permissions
Medium Severity
When the token file already exists with loose permissions (e.g., 0o644 from a pre-PR installation), the mode parameter of os.open is ignored — it only applies when O_CREAT actually creates a new file. The token is written to the still-world-readable file, and path.chmod(_SECRET_FILE_MODE) only runs afterward. Adding os.fchmod(fd, _SECRET_FILE_MODE) between os.open and f.write(content) would eliminate this window for pre-existing files.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit b38123d. Configure here.
There was a problem hiding this comment.
not a problem IMO, let's not be too paranoiac
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4234 +/- ##
==========================================
+ Coverage 75.00% 77.10% +2.10%
==========================================
Files 145 171 +26
Lines 13978 19543 +5565
==========================================
+ Hits 10484 15069 +4585
- Misses 3494 4474 +980 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
This PR has been shipped as part of the v1.16.0 release. |


Summary
huggingface_hubwrites the user's HF token files at Python's default file modes (0o666 masked by umask 022 → 0o644 on macOS Terminal and most Linux defaults), leaving them world-readable. Two writer sites:src/huggingface_hub/_login.py:441-445—Path.write_text(token)for the single active token atHF_TOKEN_PATH(~/.cache/huggingface/token).src/huggingface_hub/utils/_auth.py:157-167—_save_stored_tokenswrites the multi-account INI toHF_STORED_TOKENS_PATH(~/.cache/huggingface/stored_tokens). Worst case impact-wise — the file contains every named token the user has saved viahuggingface-cli auth login --token(personal + work + organization).Any local account or process that can traverse the home directory can recover these tokens and act as the user against the Hub API — pulling or pushing private models and datasets, running Inference Endpoint predictions on the user's credits, accessing private orgs.
This PR adds
mode=0o700to the parentmkdirandpath.chmod(0o600)/path.parent.chmod(0o700)after each write so pre-existing installations converge to safe perms on the next save. The chmod calls are wrapped in try/except for Windows (no POSIX modes).How to verify
A small Python reproducer that mirrors the pre-PR call shape:
Pre-PR:
file mode (pre-PR): 0o644 / dir mode (pre-PR): 0o755. After applying the PR:0o600 / 0o700.Industry baseline
GitHub CLI (
~/.config/gh/hosts.yml), AWS CLI (~/.aws/credentials), Google Cloud SDK (~/.config/gcloud/credentials.db), Stripe CLI, PlanetScale CLI (keyring + 0o600 fallback), Pulumi (StoreCredentialsuseslockedfile.Writeat 0o600).Related disclosure
Reported privately to
security@huggingface.coper.well-known/security.txton 2026-05-19. Filing the PR in parallel to give maintainers the ready-to-merge fix.Note
Medium Risk
Touches credential persistence paths (
HF_TOKEN_PATH/HF_STORED_TOKENS_PATH) and changes how secrets are written to disk; while small, mistakes could break login persistence or permissions across platforms.Overview
Hardens on-disk token persistence by introducing
_write_secretto write Hub tokens with restrictive permissions (directory0o700, file0o600) on POSIX systems.The login flow now uses
_write_secretinstead ofPath.write_textforHF_TOKEN_PATH, and stored-tokens saving is refactored to write the INI content via an in-memory buffer and_write_secretforHF_STORED_TOKENS_PATH, with best-effortchmodhandling on Windows.Reviewed by Cursor Bugbot for commit fca1960. Bugbot is set up for automated code reviews on this repo. Configure here.