diff --git a/.changeset/resolute-toctou.md b/.changeset/resolute-toctou.md new file mode 100644 index 00000000..9476d176 --- /dev/null +++ b/.changeset/resolute-toctou.md @@ -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. diff --git a/Cargo.toml b/Cargo.toml index 44bf0235..99966a54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } @@ -66,4 +67,3 @@ lto = "thin" [dev-dependencies] serial_test = "3.4.0" -tempfile = "3" diff --git a/src/fs_util.rs b/src/fs_util.rs index b387565e..560cfa97 100644 --- a/src/fs_util.rs +++ b/src/fs_util.rs @@ -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)]