Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/resolute-toctou.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": 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.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ reqwest = { version = "0.12", features = ["json", "stream", "rustls-tls-native-r
rand = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tempfile = "3"
sha2 = "0.10"
thiserror = "2"
tokio = { version = "1", features = ["full"] }
Expand All @@ -66,4 +67,3 @@ lto = "thin"

[dev-dependencies]
serial_test = "3.4.0"
tempfile = "3"
39 changes: 16 additions & 23 deletions src/fs_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,35 +30,28 @@ use std::path::Path;
/// Returns an `io::Error` if the temporary file cannot be written or if the
/// rename fails.
pub fn atomic_write(path: &Path, data: &[u8]) -> io::Result<()> {
// Derive a sibling tmp path, e.g. `/home/user/.config/gws/credentials.enc.tmp`
let file_name = path
.file_name()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "path has no file name"))?;
let tmp_name = format!("{}.tmp", file_name.to_string_lossy());
let tmp_path = path
.parent()
.map(|p| p.join(&tmp_name))
.unwrap_or_else(|| std::path::PathBuf::from(&tmp_name));
let parent = path.parent().unwrap_or_else(|| std::path::Path::new(""));
let mut tmp_file = tempfile::Builder::new()
.prefix(".tmp")
.make_in(parent)?;

std::fs::write(&tmp_path, data)?;
std::fs::rename(&tmp_path, path)?;
{
use std::io::Write;
tmp_file.write_all(data)?;
tmp_file.as_file_mut().sync_all()?;
}

tmp_file.persist(path).map_err(|e| e.error)?;
Ok(())
}

/// Async variant of [`atomic_write`] for use with tokio.
pub async fn atomic_write_async(path: &Path, data: &[u8]) -> io::Result<()> {
let file_name = path
.file_name()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "path has no file name"))?;
let tmp_name = format!("{}.tmp", file_name.to_string_lossy());
let tmp_path = path
.parent()
.map(|p| p.join(&tmp_name))
.unwrap_or_else(|| std::path::PathBuf::from(&tmp_name));

tokio::fs::write(&tmp_path, data).await?;
tokio::fs::rename(&tmp_path, path).await?;
Ok(())
let path = path.to_path_buf();
let data = data.to_vec();
tokio::task::spawn_blocking(move || atomic_write(&path, &data))
.await
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?
}

#[cfg(test)]
Expand Down
Loading