From 9b49bd6892497efcbf08f58fef7be03c97bf05d7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:09:20 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20fix:=20secure=20atomic=20file=20?= =?UTF-8?q?creation=20with=20restrictive=20permissions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use `0o600` permissions (read/write only by the owner) when creating temporary files in `atomic_write` and `atomic_write_async`. This prevents a race condition on Unix systems where a sensitive file (like credentials or tokens) could be created with default permissions (e.g., world-readable) before its permissions are corrected. The fix uses `OpenOptions::mode(0o600)` on Unix and also adds `sync_all()` for better data durability. A new test `test_atomic_write_permissions` verifies the fix on Unix. A changeset file is also included. Co-authored-by: jpoehnelt <3392975+jpoehnelt@users.noreply.github.com> --- .../security-fix-atomic-write-permissions.md | 5 ++ src/fs_util.rs | 62 ++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 .changeset/security-fix-atomic-write-permissions.md diff --git a/.changeset/security-fix-atomic-write-permissions.md b/.changeset/security-fix-atomic-write-permissions.md new file mode 100644 index 00000000..2fd54cbe --- /dev/null +++ b/.changeset/security-fix-atomic-write-permissions.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +🔒 Fix: Use restrictive permissions (0600) for temporary files created during atomic writes in fs_util. diff --git a/src/fs_util.rs b/src/fs_util.rs index b387565e..8b061fa5 100644 --- a/src/fs_util.rs +++ b/src/fs_util.rs @@ -40,7 +40,21 @@ 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::fs::OpenOptions; + use std::io::Write; + let mut opts = 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 +70,21 @@ 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::fs::OpenOptions; + use tokio::io::AsyncWriteExt; + let mut opts = OpenOptions::new(); + opts.write(true).create(true).truncate(true); + #[cfg(unix)] + { + use tokio::os::unix::fs::OpenOptionsExt; + 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(()) } @@ -99,4 +127,34 @@ mod tests { atomic_write_async(&path, b"async hello").await.unwrap(); assert_eq!(fs::read(&path).unwrap(), b"async hello"); } + + #[tokio::test] + async fn test_atomic_write_permissions() { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + let dir = tempfile::tempdir().unwrap(); + + // Sync + let path_sync = dir.path().join("sync.txt"); + atomic_write(&path_sync, b"sync").unwrap(); + let meta_sync = fs::metadata(&path_sync).unwrap(); + assert_eq!( + meta_sync.permissions().mode() & 0o777, + 0o600, + "Sync atomic_write should create file with 0600 permissions" + ); + + // Async + let path_async = dir.path().join("async.txt"); + atomic_write_async(&path_async, b"async").await.unwrap(); + let meta_async = fs::metadata(&path_async).unwrap(); + assert_eq!( + meta_async.permissions().mode() & 0o777, + 0o600, + "Async atomic_write_async should create file with 0600 permissions" + ); + } + } }