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::write → File::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:
- 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).
- 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:
- Remove
~/.config/rc/config.toml (first run or fresh RC_CONFIG_DIR).
- Run any command that writes the config, e.g.
rc alias set ....
- 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.
Summary
ConfigManager::savecreatesconfig.tomlwith the process-default file mode (typically0644, i.e. group/world-readable) and only tightens it to0600after the contents — which include alias access/secret keys — have already been written. Between the write and thechmod, and permanently if the process dies orset_permissionsfails in between, the credentials sit on disk world-readable.Location
crates/core/src/config.rsConfigManager::saveProblem
std::fs::write→File::createcreates missing files with mode0o666 & ~umask; with the common umask022that is0644. The restrictive mode is applied only in a second, non-atomic step. Two concrete consequences:access_key/secret_keyfor every stored alias is readable by other local users (backups, sync agents, directory watchers, or simply another process listing the directory at that moment).writebut beforeset_permissions, or ifset_permissionsreturns an error (which is propagated after the secret bytes are already on disk), the overly-broad mode persists indefinitely. The nextsave()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_filerefuses 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 being0640, while its own config store briefly (or permanently) is0644.Trigger / Reproduction
Static-analysis finding (not executed). On a Unix system:
~/.config/rc/config.toml(first run or freshRC_CONFIG_DIR).rc alias set ....0644, not0600; 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 insecret_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:
(Optionally keep the existing
set_permissionscall to tighten modes of pre-existing files created by older versions.)Evidence
crates/core/src/config.rsperformsfs::writebeforeset_permissions(0o600); Rust'sFile::createuses mode0o666 & ~umask.Config/Aliasstructs serializeaccess_keyandsecret_keyinto this file.crates/cli/src/secret_input.rsenforcesmode & 0o077 == 0on externally supplied key files, establishing the project's own sensitivity to exactly this class of exposure.