🔒 fix: secure atomic file creation with restrictive permissions

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>
This commit is contained in:
google-labs-jules[bot]
2026-03-10 22:09:20 +00:00
parent ad99aa9a2a
commit 9b49bd6892
2 changed files with 65 additions and 2 deletions
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---
🔒 Fix: Use restrictive permissions (0600) for temporary files created during atomic writes in fs_util.
+60 -2
View File
@@ -40,7 +40,21 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> io::Result<()> {
.map(|p| p.join(&tmp_name)) .map(|p| p.join(&tmp_name))
.unwrap_or_else(|| std::path::PathBuf::from(&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)?; std::fs::rename(&tmp_path, path)?;
Ok(()) Ok(())
} }
@@ -56,7 +70,21 @@ pub async fn atomic_write_async(path: &Path, data: &[u8]) -> io::Result<()> {
.map(|p| p.join(&tmp_name)) .map(|p| p.join(&tmp_name))
.unwrap_or_else(|| std::path::PathBuf::from(&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?; tokio::fs::rename(&tmp_path, path).await?;
Ok(()) Ok(())
} }
@@ -99,4 +127,34 @@ mod tests {
atomic_write_async(&path, b"async hello").await.unwrap(); atomic_write_async(&path, b"async hello").await.unwrap();
assert_eq!(fs::read(&path).unwrap(), b"async hello"); 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"
);
}
}
} }