diff --git a/cmd/soroban-cli/src/commands/snapshot/create.rs b/cmd/soroban-cli/src/commands/snapshot/create.rs index 966a6a5992..8fa39f575b 100644 --- a/cmd/soroban-cli/src/commands/snapshot/create.rs +++ b/cmd/soroban-cli/src/commands/snapshot/create.rs @@ -647,6 +647,7 @@ async fn get_ledger_metadata_from_archive( } fs::rename(&dl_path, &cache_path).map_err(Error::RenameDownloadFile)?; + let _ = crate::config::locator::set_hardened_permissions(&cache_path); print.clear_previous_line(); print.globeln(format!("Downloaded ledger headers for ledger {ledger}")); @@ -760,6 +761,7 @@ async fn cache_bucket( } fs::rename(&dl_path, &cache_path).map_err(Error::RenameDownloadFile)?; + let _ = crate::config::locator::set_hardened_permissions(&cache_path); } Ok(cache_path) } diff --git a/cmd/soroban-cli/src/commands/tx/edit.rs b/cmd/soroban-cli/src/commands/tx/edit.rs index 8bed4200ef..c859ba69b4 100644 --- a/cmd/soroban-cli/src/commands/tx/edit.rs +++ b/cmd/soroban-cli/src/commands/tx/edit.rs @@ -1,7 +1,7 @@ use std::{ env, fs::{self}, - io::{stdin, Cursor, IsTerminal, Write}, + io::{stdin, Cursor, IsTerminal}, path::PathBuf, process::{self}, }; @@ -82,21 +82,12 @@ fn tmp_file(contents: &str) -> Result<(TempDir, PathBuf), Error> { let path = temp_dir.path().join("edit.json"); #[cfg(unix)] - let mut file = { - use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; + { + use std::os::unix::fs::PermissionsExt; fs::set_permissions(temp_dir.path(), fs::Permissions::from_mode(0o700))?; - fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .mode(0o600) - .open(&path)? - }; - - #[cfg(not(unix))] - let mut file = fs::File::create(&path)?; - - file.write_all(contents.as_bytes())?; + } + + crate::config::locator::write_hardened_file(&path, contents.as_bytes())?; Ok((temp_dir, path)) } diff --git a/cmd/soroban-cli/src/config/data.rs b/cmd/soroban-cli/src/config/data.rs index a1cbde20b2..0d227bb087 100644 --- a/cmd/soroban-cli/src/config/data.rs +++ b/cmd/soroban-cli/src/config/data.rs @@ -65,7 +65,7 @@ pub fn write(action: Action, rpc_url: &Url) -> Result { }; let id = ulid::Ulid::new(); let file = actions_dir()?.join(id.to_string()).with_extension("json"); - std::fs::write(file, serde_json::to_string(&data)?)?; + crate::config::locator::write_hardened_file(&file, serde_json::to_string(&data)?.as_bytes())?; Ok(id) } @@ -82,7 +82,7 @@ pub fn write_spec(hash: &str, spec_entries: &[xdr::ScSpecEntry]) -> Result<(), E for entry in spec_entries { contents.extend(entry.to_xdr(xdr::Limits::none())?); } - std::fs::write(file, contents)?; + crate::config::locator::write_hardened_file(&file, &contents)?; Ok(()) } diff --git a/cmd/soroban-cli/src/config/locator.rs b/cmd/soroban-cli/src/config/locator.rs index 32b107303d..cc44943a1e 100644 --- a/cmd/soroban-cli/src/config/locator.rs +++ b/cmd/soroban-cli/src/config/locator.rs @@ -4,8 +4,7 @@ use serde::de::DeserializeOwned; use std::{ ffi::OsStr, fmt::Display, - fs::{self, OpenOptions}, - io::{self, Write}, + fs, io, path::{Path, PathBuf}, str::FromStr, }; @@ -483,32 +482,11 @@ impl Args { .insert(network_passphrase.into(), contract_id.to_string()); let content = serde_json::to_string(&data)?; + write_hardened_file(&path, content.as_bytes())?; #[cfg(unix)] - { - use std::io::Write as _; - use std::os::unix::fs::OpenOptionsExt; - let mut to_file = OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .mode(0o600) - .open(&path)?; - to_file.write_all(content.as_bytes())?; - set_hardened_permissions(&path)?; - if let Ok(root) = self.config_dir() { - fix_config_permissions(root); - } - } - - #[cfg(not(unix))] - { - let mut to_file = OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(path)?; - to_file.write_all(content.as_bytes())?; + if let Ok(root) = self.config_dir() { + fix_config_permissions(root); } Ok(()) @@ -524,17 +502,11 @@ impl Args { let content = fs::read_to_string(&path).unwrap_or_default(); let mut data: alias::Data = serde_json::from_str(&content).unwrap_or_default(); - let mut to_file = OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(path)?; - data.ids.remove::(network_passphrase); let content = serde_json::to_string(&data)?; - - Ok(to_file.write_all(content.as_bytes())?) + write_hardened_file(&path, content.as_bytes())?; + Ok(()) } pub fn get_contract_id( @@ -662,6 +634,31 @@ pub(crate) fn set_hardened_permissions(path: &Path) -> io::Result<()> { Ok(()) } +/// Writes `contents` to `path`, creating the file with `0600` on Unix and +/// resetting the mode to exactly `0600` afterwards regardless of any +/// pre-existing permissions. Falls back to `std::fs::write` on non-Unix +/// platforms. +pub(crate) fn write_hardened_file(path: &Path, contents: &[u8]) -> io::Result<()> { + #[cfg(unix)] + { + use std::io::Write as _; + use std::os::unix::fs::OpenOptionsExt; + let mut file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(path)?; + file.write_all(contents)?; + set_hardened_permissions(path)?; + } + + #[cfg(not(unix))] + std::fs::write(path, contents)?; + + Ok(()) +} + pub fn ensure_directory(dir: PathBuf) -> Result { let parent = dir.parent().ok_or(Error::HomeDirNotFound)?; @@ -757,41 +754,15 @@ impl KeyType { ) -> Result { let filepath = ensure_directory(self.path(pwd, key))?; let data = toml::to_string(value).map_err(|_| Error::ConfigSerialization)?; - #[cfg(unix)] - { - use std::io::Write as _; - use std::os::unix::fs::OpenOptionsExt; - let mut file = std::fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .mode(0o600) - .open(&filepath) - .map_err(|error| Error::IdCreationFailed { - filepath: filepath.clone(), - error, - })?; - file.write_all(data.as_bytes()) - .map_err(|error| Error::IdCreationFailed { - filepath: filepath.clone(), - error, - })?; - } - - #[cfg(not(unix))] - std::fs::write(&filepath, data).map_err(|error| Error::IdCreationFailed { - filepath: filepath.clone(), - error, + write_hardened_file(&filepath, data.as_bytes()).map_err(|error| { + Error::IdCreationFailed { + filepath: filepath.clone(), + error, + } })?; #[cfg(unix)] - { - set_hardened_permissions(&filepath).map_err(|error| Error::IdCreationFailed { - filepath: filepath.clone(), - error, - })?; - fix_config_permissions(pwd.to_path_buf()); - } + fix_config_permissions(pwd.to_path_buf()); Ok(filepath) } diff --git a/cmd/soroban-cli/src/config/mod.rs b/cmd/soroban-cli/src/config/mod.rs index b980b447bb..2b0d049783 100644 --- a/cmd/soroban-cli/src/config/mod.rs +++ b/cmd/soroban-cli/src/config/mod.rs @@ -1,8 +1,5 @@ use serde::{Deserialize, Serialize}; -use std::{ - fs::{self, File}, - io::Write, -}; +use std::fs; use crate::{ commands::HEADING_TRANSACTION, @@ -294,9 +291,8 @@ impl Config { pub fn save_to(&self, path: &std::path::Path) -> Result<(), locator::Error> { let toml_string = toml::to_string(&self)?; - // Depending on the platform, this function may fail if the full directory path does not exist - let mut file = File::create(locator::ensure_directory(path.to_path_buf())?)?; - file.write_all(toml_string.as_bytes())?; + let path = locator::ensure_directory(path.to_path_buf())?; + locator::write_hardened_file(&path, toml_string.as_bytes())?; Ok(()) } } diff --git a/cmd/soroban-cli/src/config/upgrade_check.rs b/cmd/soroban-cli/src/config/upgrade_check.rs index 4c7b6d22ac..cfa9b40a05 100644 --- a/cmd/soroban-cli/src/config/upgrade_check.rs +++ b/cmd/soroban-cli/src/config/upgrade_check.rs @@ -61,7 +61,7 @@ impl UpgradeCheck { let path = locator::ensure_directory(path)?; let data = serde_json::to_string(self).map_err(|_| locator::Error::ConfigSerialization)?; - fs::write(&path, data) + locator::write_hardened_file(&path, data.as_bytes()) .map_err(|error| locator::Error::UpgradeCheckWriteFailed { path, error }) } } @@ -100,4 +100,21 @@ mod tests { let loaded_check = UpgradeCheck::load().unwrap(); assert_eq!(loaded_check, saved_check); } + + #[cfg(unix)] + #[test] + #[serial] + fn test_upgrade_check_save_uses_0600_permissions() { + use crate::test_utils::with_env_set; + use std::os::unix::fs::PermissionsExt; + + let temp_dir = tempfile::tempdir().unwrap(); + with_env_set("STELLAR_DATA_HOME", temp_dir.path(), || { + UpgradeCheck::default().save().unwrap(); + + let path = project_dir().unwrap().data_dir().join(FILE_NAME); + let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777; + assert_eq!(mode, 0o600, "expected 0600, got {mode:o}"); + }); + } }