diff --git a/.changeset/resolute-toctou.md b/.changeset/resolute-toctou.md new file mode 100644 index 00000000..9104032d --- /dev/null +++ b/.changeset/resolute-toctou.md @@ -0,0 +1,5 @@ +--- +"gws": patch +--- + +Resolve TOCTOU race condition in `fs_util::atomic_write` and `atomic_write_async` to securely enforce 0600 file permissions upon file creation, preventing intermediate local read access to secrets. diff --git a/src/credential_store.rs b/src/credential_store.rs index a0210fc2..d697c446 100644 --- a/src/credential_store.rs +++ b/src/credential_store.rs @@ -391,18 +391,6 @@ pub fn save_encrypted(json: &str) -> anyhow::Result { crate::fs_util::atomic_write(&path, &encrypted) .map_err(|e| anyhow::anyhow!("Failed to write credentials: {e}"))?; - // Set permissions to 600 on Unix (contains secrets) - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - if let Err(e) = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)) { - eprintln!( - "Warning: failed to set file permissions on {}: {e}", - path.display() - ); - } - } - Ok(path) } diff --git a/src/fs_util.rs b/src/fs_util.rs index b387565e..f86aa315 100644 --- a/src/fs_util.rs +++ b/src/fs_util.rs @@ -40,7 +40,19 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> io::Result<()> { .map(|p| p.join(&tmp_name)) .unwrap_or_else(|| std::path::PathBuf::from(&tmp_name)); - std::fs::write(&tmp_path, data)?; + { + use std::io::Write; + let mut opts = std::fs::OpenOptions::new(); + opts.write(true).create(true).truncate(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + opts.mode(0o600); + } + let mut file = opts.open(&tmp_path)?; + file.write_all(data)?; + file.sync_all()?; + } std::fs::rename(&tmp_path, path)?; Ok(()) } @@ -56,7 +68,18 @@ pub async fn atomic_write_async(path: &Path, data: &[u8]) -> io::Result<()> { .map(|p| p.join(&tmp_name)) .unwrap_or_else(|| std::path::PathBuf::from(&tmp_name)); - tokio::fs::write(&tmp_path, data).await?; + { + use tokio::io::AsyncWriteExt; + let mut opts = tokio::fs::OpenOptions::new(); + opts.write(true).create(true).truncate(true); + #[cfg(unix)] + { + opts.mode(0o600); + } + let mut file = opts.open(&tmp_path).await?; + file.write_all(data).await?; + file.sync_all().await?; + } tokio::fs::rename(&tmp_path, path).await?; Ok(()) } diff --git a/src/oauth_config.rs b/src/oauth_config.rs index 02154b58..89f34535 100644 --- a/src/oauth_config.rs +++ b/src/oauth_config.rs @@ -84,13 +84,6 @@ pub fn save_client_config( crate::fs_util::atomic_write(&path, json.as_bytes()) .map_err(|e| anyhow::anyhow!("Failed to write client config: {e}"))?; - // Set file permissions to 600 on Unix (contains secrets) - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?; - } - Ok(path) }