Skip to content

Secret key file created with world-readable permissions before chmod (TOCTOU race on Unix) #2360

Closed

Description

@MootSeeker

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:

  1. On a multi-user Linux/macOS system, set umask to 022 (default).
  2. Run OpenHuman for the first time (triggers .secret_key creation).
  3. A concurrent process monitoring ~/.openhuman/ via inotifywait or a tight polling loop can read the key file in the window between fs::write and fs::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_key is never world-readable at any point during creation (verified with inotifywait + immediate stat).
  • Regression safety — Unit test added that verifies file permissions are 0600 immediately after SecretStore::new triggers 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_permissions patterns remain for sensitive files.

Related

Activity

  1. MootSeeker commented on May 20, 2026

    @MootSeeker
    ContributorAuthor

    Working on this — fix branch pushed at MootSeeker/openhuman:fix/secret-key-toctou-race.

    The fix replaces fs::write + fs::set_permissions with OpenOptions::new().mode(0o600) to create the file with restricted permissions atomically, matching the existing secure pattern in src/core/auth.rs:write_token_file. PR incoming shortly.

  2. added 2 commits that reference this issue on Sep 1, 2026
    46e31e2
    1ba1982
  3. added a commit that references this issue on Sep 11, 2026
    486eb63
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions