Skip to content

config.toml containing alias credentials is created world-readable (0644) and chmod'd to 0600 only afterwards #357

Description

@krishna3554

Summary

ConfigManager::save creates config.toml with the process-default file mode (typically 0644, i.e. group/world-readable) and only tightens it to 0600 after the contents — which include alias access/secret keys — have already been written. Between the write and the chmod, and permanently if the process dies or set_permissions fails in between, the credentials sit on disk world-readable.

Location

  • File: crates/core/src/config.rs
  • Function: ConfigManager::save
  • Relevant code:
let content = toml::to_string_pretty(config)?;
std::fs::write(&self.config_path, content)?;   // created with 0666 & ~umask (typically 0644)

// Set restrictive permissions on Unix systems
#[cfg(unix)]
{
    use std::os::unix::fs::PermissionsExt as _;
    let permissions = std::fs::Permissions::from_mode(0o600);
    std::fs::set_permissions(&self.config_path, permissions)?;
}

Problem

std::fs::writeFile::create creates missing files with mode 0o666 & ~umask; with the common umask 022 that is 0644. The restrictive mode is applied only in a second, non-atomic step. Two concrete consequences:

  1. There is a window in which the file containing access_key / secret_key for every stored alias is readable by other local users (backups, sync agents, directory watchers, or simply another process listing the directory at that moment).
  2. If the process crashes or is killed after write but before set_permissions, or if set_permissions returns an error (which is propagated after the secret bytes are already on disk), the overly-broad mode persists indefinitely. The next save() rewrites content via truncate-in-place, which keeps the existing (wrong) mode rather than resetting it.

This is also inconsistent with the standard this codebase itself sets elsewhere: crates/cli/src/secret_input.rs::read_protected_key_file refuses to read an SSE-C key file whose mode grants group/other any access (mode & 0o077 != 0). So rc rejects a user's key file for being 0640, while its own config store briefly (or permanently) is 0644.

Trigger / Reproduction

Static-analysis finding (not executed). On a Unix system:

  1. Remove ~/.config/rc/config.toml (first run or fresh RC_CONFIG_DIR).
  2. Run any command that writes the config, e.g. rc alias set ....
  3. Observe the file mode immediately after creation — before/without the chmod completing it is 0644, not 0600; a crash between the two syscalls leaves it that way.

Expected Behavior

The configuration file should never exist at a mode broader than 0600: create it atomically with the restrictive mode from the start (e.g. OpenOptions::new().write(true).create(true).truncate(true).mode(0o600) on Unix), matching the care applied to SSE-C key material in secret_input.rs.

Actual Behavior

The file is created first with the umask-derived mode and tightened afterwards, leaving a non-atomic gap and a persistent-wrong-mode failure path.

Impact

Local exposure of all stored alias credentials (access key IDs and secret keys) during every config write, with a permanent-exposure path on crash/failure. The repo's own threat model treats on-disk key permissions as important (secret_input.rs), so the same bar should apply to the credential store itself.

Suggested Direction

On Unix, open with explicit mode:

#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;
std::fs::OpenOptions::new()
    .write(true)
    .create(true)
    .truncate(true)
    .mode(0o600)
    .open(&self.config_path)?
    .write_all(content.as_bytes())?;

(Optionally keep the existing set_permissions call to tighten modes of pre-existing files created by older versions.)

Evidence

  • crates/core/src/config.rs performs fs::write before set_permissions(0o600); Rust's File::create uses mode 0o666 & ~umask.
  • The config Config/Alias structs serialize access_key and secret_key into this file.
  • crates/cli/src/secret_input.rs enforces mode & 0o077 == 0 on externally supplied key files, establishing the project's own sensitivity to exactly this class of exposure.

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