Skip to content
Merged
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
2 changes: 2 additions & 0 deletions cmd/soroban-cli/src/commands/snapshot/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}"));
Expand Down Expand Up @@ -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)
}
Expand Down
21 changes: 6 additions & 15 deletions cmd/soroban-cli/src/commands/tx/edit.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{
env,
fs::{self},
io::{stdin, Cursor, IsTerminal, Write},
io::{stdin, Cursor, IsTerminal},
path::PathBuf,
process::{self},
};
Expand Down Expand Up @@ -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))
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/soroban-cli/src/config/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ pub fn write(action: Action, rpc_url: &Url) -> Result<ulid::Ulid, Error> {
};
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)
}

Expand All @@ -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(())
}

Expand Down
103 changes: 37 additions & 66 deletions cmd/soroban-cli/src/config/locator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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(())
Expand All @@ -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::<str>(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(
Expand Down Expand Up @@ -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<PathBuf, Error> {
let parent = dir.parent().ok_or(Error::HomeDirNotFound)?;

Expand Down Expand Up @@ -757,41 +754,15 @@ impl KeyType {
) -> Result<PathBuf, Error> {
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)
}
Expand Down
10 changes: 3 additions & 7 deletions cmd/soroban-cli/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
use serde::{Deserialize, Serialize};
use std::{
fs::{self, File},
io::Write,
};
use std::fs;

use crate::{
commands::HEADING_TRANSACTION,
Expand Down Expand Up @@ -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(())
}
}
19 changes: 18 additions & 1 deletion cmd/soroban-cli/src/config/upgrade_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
}
Expand Down Expand Up @@ -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}");
});
}
}
Loading