From d03da8ad718b2e18df5227564b612dd7cf0e37fc Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:09:15 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[MEDIUM]=20?= =?UTF-8?q?Fix=20insecure=20file=20permissions=20on=20lockfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 Severity: MEDIUM 💡 Vulnerability: The `ProfileLock` struct creates lock files and their parent directories using default system permissions (`fs::create_dir_all` and `fs::OpenOptions::new()`), which could expose sensitive config paths or metadata on shared systems. 🎯 Impact: Local privilege escalation or info leakage of profile presence/metadata if executed on a shared multi-user machine. 🔧 Fix: Updated `ProfileLock::acquire` to use `crate::paths::create_secure_dir_all` (mode `0o700`) for the parent directory and explicitly set mode `0o600` for the lockfile itself via `OpenOptionsExt` on Unix platforms. Also cleaned up `cargo fmt` errors across codebase. ✅ Verification: `cargo test`, `cargo clippy`, and `cargo fmt` execute successfully. Check `.lock` file permissions after running the CLI. Co-authored-by: bitcoiner-dev <75873427+bitcoiner-dev@users.noreply.github.com> --- src/commands/account.rs | 2 +- src/commands/address.rs | 2 +- src/commands/doctor.rs | 2 +- src/commands/inscription.rs | 6 +----- src/commands/lock.rs | 2 +- src/commands/mod.rs | 2 +- src/commands/scenario.rs | 6 ++---- src/commands/wallet.rs | 12 ++++++++++-- src/dashboard.rs | 7 +------ src/lock.rs | 15 +++++++++++---- src/paths.rs | 2 +- src/presenter/grid.rs | 30 +++++++++++++++++++----------- 12 files changed, 50 insertions(+), 38 deletions(-) diff --git a/src/commands/account.rs b/src/commands/account.rs index 77b6001..1aa5d6d 100644 --- a/src/commands/account.rs +++ b/src/commands/account.rs @@ -1,8 +1,8 @@ use crate::cli::{AccountAction, AccountArgs, Cli}; use crate::error::AppError; +use crate::output::CommandOutput; use crate::wallet_service::{now_unix, AccountState}; use crate::{load_wallet_session, profile_path, read_profile, write_profile}; -use crate::output::CommandOutput; use zinc_core::Account; pub async fn run(cli: &Cli, args: &AccountArgs) -> Result { diff --git a/src/commands/address.rs b/src/commands/address.rs index 03ec922..29376dd 100644 --- a/src/commands/address.rs +++ b/src/commands/address.rs @@ -1,8 +1,8 @@ use crate::cli::{AddressArgs, AddressKind, Cli}; use crate::error::AppError; +use crate::output::CommandOutput; use crate::wallet_service::map_wallet_error; use crate::{load_wallet_session, persist_wallet_session}; -use crate::output::CommandOutput; pub async fn run(cli: &Cli, args: &AddressArgs) -> Result { let mut session = load_wallet_session(cli)?; diff --git a/src/commands/doctor.rs b/src/commands/doctor.rs index c21ac71..d76be3c 100644 --- a/src/commands/doctor.rs +++ b/src/commands/doctor.rs @@ -1,7 +1,7 @@ use crate::cli::Cli; use crate::error::AppError; -use crate::{profile_path, read_profile}; use crate::output::CommandOutput; +use crate::{profile_path, read_profile}; use zinc_core::{OrdClient, ZincWallet}; pub async fn run(cli: &Cli) -> Result { diff --git a/src/commands/inscription.rs b/src/commands/inscription.rs index 01bd0d7..c7e77a4 100644 --- a/src/commands/inscription.rs +++ b/src/commands/inscription.rs @@ -12,11 +12,7 @@ pub async fn run(cli: &Cli, _args: &InscriptionArgs) -> Result Result { diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e97ad3b..10dd78f 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -12,6 +12,6 @@ pub mod setup; pub mod snapshot; pub mod sync; pub mod tx; +pub mod version; pub mod wait; pub mod wallet; -pub mod version; diff --git a/src/commands/scenario.rs b/src/commands/scenario.rs index e38cabb..9dff346 100644 --- a/src/commands/scenario.rs +++ b/src/commands/scenario.rs @@ -1,9 +1,9 @@ use crate::cli::{Cli, ScenarioAction, ScenarioArgs}; use crate::error::AppError; +use crate::output::CommandOutput; use crate::utils::run_bitcoin_cli; use crate::wallet_service::NetworkArg; use crate::{confirm, load_wallet_session, persist_wallet_session, snapshot_dir}; -use crate::output::CommandOutput; use std::fs; pub async fn run(cli: &Cli, args: &ScenarioArgs) -> Result { @@ -121,9 +121,7 @@ pub async fn run(cli: &Cli, args: &ScenarioArgs) -> Result Result Result { diff --git a/src/dashboard.rs b/src/dashboard.rs index 26db6fa..19bcb15 100644 --- a/src/dashboard.rs +++ b/src/dashboard.rs @@ -175,12 +175,7 @@ fn activate_session( state.network = Some(resolved_network.value); state.profile_name = Some(resolved_profile); - state.ordinals_address = Some( - session - .wallet - .peek_taproot_address(0) - .to_string(), - ); + state.ordinals_address = Some(session.wallet.peek_taproot_address(0).to_string()); state.payment_address = session .wallet .peek_payment_address(0) diff --git a/src/lock.rs b/src/lock.rs index 7edd7d9..5d50682 100644 --- a/src/lock.rs +++ b/src/lock.rs @@ -22,14 +22,21 @@ impl ProfileLock { let lock_path = profile_path.with_extension("lock"); if let Some(parent) = lock_path.parent() { - fs::create_dir_all(parent).map_err(|e| { + crate::paths::create_secure_dir_all(parent).map_err(|e| { crate::error::AppError::Config(format!("failed to create lock dir: {e}")) })?; } - let file = fs::OpenOptions::new() - .write(true) - .create_new(true) + let mut options = fs::OpenOptions::new(); + options.write(true).create_new(true); + + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + + let file = options .open(&lock_path) .map_err(|_| AppError::Config("profile is locked by another instance".to_string()))?; diff --git a/src/paths.rs b/src/paths.rs index 7f0f27c..e9be32c 100644 --- a/src/paths.rs +++ b/src/paths.rs @@ -25,8 +25,8 @@ pub fn write_secure_file>(path: P, contents: &[u8]) -> std::io::R let path = path.as_ref(); #[cfg(unix)] { - use std::os::unix::fs::OpenOptionsExt; use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; let mut options = fs::OpenOptions::new(); options.write(true).create(true).truncate(true).mode(0o600); let mut file = options.open(path)?; diff --git a/src/presenter/grid.rs b/src/presenter/grid.rs index 5ce042f..05a789a 100644 --- a/src/presenter/grid.rs +++ b/src/presenter/grid.rs @@ -73,11 +73,7 @@ pub fn render_grid(cards: &[GridCard], max_cols: usize, gutter: usize) -> String for line_idx in 0..max_height { for (card_idx, card) in row_cards.iter().enumerate() { - let line = card - .lines - .get(line_idx) - .map(|s| s.as_str()) - .unwrap_or(""); + let line = card.lines.get(line_idx).map(|s| s.as_str()).unwrap_or(""); // Pad every card to cell_width so columns stay aligned. out.push_str(&pad_to_visible(line, cell_width)); @@ -119,9 +115,15 @@ mod tests { #[test] fn grid_arranges_cards_side_by_side() { let cards = vec![ - GridCard { lines: vec!["AAAA".into(), "AAAA".into()] }, - GridCard { lines: vec!["BBBB".into(), "BBBB".into()] }, - GridCard { lines: vec!["CCCC".into()] }, // shorter card + GridCard { + lines: vec!["AAAA".into(), "AAAA".into()], + }, + GridCard { + lines: vec!["BBBB".into(), "BBBB".into()], + }, + GridCard { + lines: vec!["CCCC".into()], + }, // shorter card ]; let output = render_grid(&cards, 20, 2); // With cell_width=4, gutter=2, cols_per_row = (20+2)/(4+2) = 3 @@ -138,9 +140,15 @@ mod tests { #[test] fn grid_wraps_to_multiple_rows() { let cards = vec![ - GridCard { lines: vec!["AAAA".into()] }, - GridCard { lines: vec!["BBBB".into()] }, - GridCard { lines: vec!["CCCC".into()] }, + GridCard { + lines: vec!["AAAA".into()], + }, + GridCard { + lines: vec!["BBBB".into()], + }, + GridCard { + lines: vec!["CCCC".into()], + }, ]; // max_cols=10, gutter=2 → cell_width=4, cols_per_row = (10+2)/(4+2) = 2 let output = render_grid(&cards, 10, 2);