-
Notifications
You must be signed in to change notification settings - Fork 784
🔒 [security] Secure atomic file creation with restrictive permissions #391
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@googleworkspace/cli": patch | ||
| --- | ||
|
|
||
| 🔒 Fix: Use restrictive permissions (0600) for temporary files created during atomic writes in fs_util. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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?; | ||
| } | ||
|
Comment on lines
+73
to
+86
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The implementation of atomic writes uses a predictable temporary filename (
To remediate this, use a unique, non-predictable temporary filename and ensure it is created exclusively. The recommended approach is to use the use std::time::{SystemTime, UNIX_EPOCH};
let pid = std::process::id();
let rand_part = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
let tmp_name = format!("{}.{}-{}.tmp", file_name.to_string_lossy(), pid, rand_part);This change should be applied where |
||
|
|
||
| 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" | ||
| ); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation of atomic writes uses a predictable temporary filename (
<filename>.tmp) and opens it without theO_EXCLflag (viacreate_new(true)). This presents two security risks:0666), the application will reuse the existing file and its permissive permissions, allowing the attacker to read sensitive data. This undermines the security fix intended by this PR.To remediate this, use a unique, non-predictable temporary filename and ensure it is created exclusively. The recommended approach is to use the
tempfilecrate'sNamedTempFileorBuilderto create a temporary file with a random suffix in the target directory. If a predictable name must be used, ensurecreate_new(true)is used and handle the case where the file already exists by either failing or using a different name. Alternatively, you could generate a semi-random name, for example by including a timestamp and process ID:This change should be applied where
tmp_nameis defined.