Repository navigation
Secret key file created with world-readable permissions before chmod (TOCTOU race on Unix) #2360
Description
Summary
The .secret_key file (master encryption key for all stored credentials) is created with default umask-derived permissions (typically 0644 / world-readable) before fs::set_permissions restricts it to 0600. Another local user can read the key during this race window.
Problem
What happened: In src/openhuman/security/secrets.rs (lines ~244–253), the key file is written using fs::write() followed by a separate fs::set_permissions(..., 0o600) call:
fs::write(&self.key_path, hex_encode(&key))
.context("Failed to write secret key file")?;
// Set restrictive permissions
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&self.key_path, fs::Permissions::from_mode(0o600))
.context("Failed to set key file permissions")?;
}fs::write() uses OpenOptions::new().write(true).create(true).truncate(true) internally — the file inherits the process's umask (commonly 022), making it 0644 (owner-read-write, world-readable) until the subsequent set_permissions call runs.
What was expected: The file should be created atomically with restrictive permissions (0600), exactly as src/core/auth.rs:write_token_file already does correctly:
#[cfg(unix)]
{
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600) // <- permissions set at creation time
.open(path)?;
file.write_all(token.as_bytes())?;
}Impact: On multi-user systems (shared servers, university workstations, CI runners with multiple agents under different UIDs), any local user can race the file creation to read the 256-bit ChaCha20-Poly1305 master key. With this key, an attacker can decrypt all stored credentials (API keys, OAuth tokens, etc.) from the config file.
Steps to reproduce:
- On a multi-user Linux/macOS system, set umask to
022(default). - Run OpenHuman for the first time (triggers
.secret_keycreation). - A concurrent process monitoring
~/.openhuman/viainotifywaitor a tight polling loop can read the key file in the window betweenfs::writeandfs::set_permissions.
Severity: Medium — requires local access and precise timing, but the blast radius (all encrypted credentials) is high.
Version / platform: All versions on Unix (Linux, macOS). Windows uses a separate icacls path.
Solution
Replace fs::write + fs::set_permissions with atomic OpenOptions::new().mode(0o600):
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&self.key_path)
.context("Failed to create secret key file")?;
file.write_all(hex_encode(&key).as_bytes())
.context("Failed to write secret key file")?;
}
#[cfg(not(unix))]
{
fs::write(&self.key_path, hex_encode(&key))
.context("Failed to write secret key file")?;
}This matches the pattern already used in src/core/auth.rs:write_token_file (line ~255).
Acceptance criteria
- Repro gone —
.secret_keyis never world-readable at any point during creation (verified withinotifywait+ immediatestat). - Regression safety — Unit test added that verifies file permissions are
0600immediately afterSecretStore::newtriggers key creation (on Unix). - Diff coverage ≥ 80% — the fix PR meets the changed-lines coverage gate (Vitest + cargo-llvm-cov, enforced by
.github/workflows/coverage.yml). - Consistency — Audit confirms no other
fs::write+set_permissionspatterns remain for sensitive files.
Related
src/core/auth.rs:write_token_file— already uses the secureOpenOptions::mode(0o600)pattern (correct reference implementation).- Windows sign-in fails due to unreadable local
.secret_key#1742 — Windows-specific.secret_keyACL issue (closed, different root cause). - Security: Core RPC server uses wildcard CORS (
Access-Control-Allow-Origin: *) #2262 — CORS wildcard (related security hardening). - CWE-377 (Insecure Temporary File) / CWE-362 (Race Condition).
Activity
MootSeeker commentedon May 20, 2026
Working on this — fix branch pushed at
MootSeeker/openhuman:fix/secret-key-toctou-race.The fix replaces
fs::write+fs::set_permissionswithOpenOptions::new().mode(0o600)to create the file with restricted permissions atomically, matching the existing secure pattern insrc/core/auth.rs:write_token_file. PR incoming shortly.fix(credentials): create the auth profile store owner-only
fix(credentials): create the auth profile store owner-only
fix(credentials): create the auth profile store owner-only\n\n`fs::wr…