From 769b8cef57e2bd69fef15e3ded753407be90aaaf Mon Sep 17 00:00:00 2001 From: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> Date: Mon, 11 May 2026 10:39:58 +0200 Subject: [PATCH 1/7] fix(bash-hook): Properly shell-escape bash-hook args Fixes [SDK-1193](https://linear.app/getsentry/issue/SDK-1193/sentry-cli-fix-bash-hook-shell-injection) #skip-changelog --- src/bashsupport.sh | 4 +- src/commands/bash_hook.rs | 39 +++++++++++++------ .../bash_hook/bash_hook-release-env.trycmd | 6 +-- .../_cases/bash_hook/bash_hook-release.trycmd | 6 +-- .../_cases/bash_hook/bash_hook-tags.trycmd | 6 +-- .../_cases/bash_hook/bash_hook.trycmd | 4 +- 6 files changed, 41 insertions(+), 24 deletions(-) diff --git a/src/bashsupport.sh b/src/bashsupport.sh index 1aa0b7eee9..36319a8e43 100644 --- a/src/bashsupport.sh +++ b/src/bashsupport.sh @@ -1,5 +1,5 @@ -_SENTRY_TRACEBACK_FILE="___SENTRY_TRACEBACK_FILE___" -_SENTRY_LOG_FILE="___SENTRY_LOG_FILE___" +_SENTRY_TRACEBACK_FILE=___SENTRY_TRACEBACK_FILE___ +_SENTRY_LOG_FILE=___SENTRY_LOG_FILE___ if [ "${SENTRY_CLI_NO_EXIT_TRAP-0}" != 1 ]; then trap _sentry_exit_trap EXIT diff --git a/src/commands/bash_hook.rs b/src/commands/bash_hook.rs index 16078e4b60..125e00b8c4 100644 --- a/src/commands/bash_hook.rs +++ b/src/commands/bash_hook.rs @@ -208,6 +208,10 @@ fn send_event( Ok(()) } +fn shell_quote(value: &str) -> String { + format!("'{}'", value.replace('\'', r"'\''")) +} + pub fn execute(matches: &ArgMatches) -> Result<()> { let release = Config::current().get_release(matches).ok(); @@ -235,15 +239,18 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { let mut script = BASH_SCRIPT .replace( "___SENTRY_TRACEBACK_FILE___", - &traceback.display().to_string(), + &shell_quote(&traceback.display().to_string()), ) - .replace("___SENTRY_LOG_FILE___", &log.display().to_string()); + .replace( + "___SENTRY_LOG_FILE___", + &shell_quote(&log.display().to_string()), + ); script = script.replace( " ___SENTRY_TAGS___", &tags .iter() - .map(|tag| format!(" --tag \"{tag}\"")) + .map(|tag| format!(" --tag {}", shell_quote(tag))) .collect::>() .join(""), ); @@ -251,20 +258,17 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { script = match release { Some(release) => script.replace( " ___SENTRY_RELEASE___", - format!(" --release \"{release}\"").as_str(), + format!(" --release {}", shell_quote(&release)).as_str(), ), None => script.replace(" ___SENTRY_RELEASE___", ""), }; script = script.replace( "___SENTRY_CLI___", - matches - .get_one::("cli") - .map_or_else( - || env::current_exe().unwrap().display().to_string(), - String::clone, - ) - .as_str(), + &shell_quote(&matches.get_one::("cli").map_or_else( + || env::current_exe().unwrap().display().to_string(), + String::clone, + )), ); if matches.get_flag("no_environ") { @@ -279,3 +283,16 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { println!("{script}"); Ok(()) } + +#[cfg(test)] +mod tests { + use super::shell_quote; + + #[test] + fn shell_quote_handles_special_characters() { + assert_eq!( + shell_quote("it's $(unsafe); && ok"), + "'it'\\''s $(unsafe); && ok'" + ); + } +} diff --git a/tests/integration/_cases/bash_hook/bash_hook-release-env.trycmd b/tests/integration/_cases/bash_hook/bash_hook-release-env.trycmd index 34b4869672..60188b8786 100644 --- a/tests/integration/_cases/bash_hook/bash_hook-release-env.trycmd +++ b/tests/integration/_cases/bash_hook/bash_hook-release-env.trycmd @@ -3,8 +3,8 @@ $ SENTRY_RELEASE=0.2.0 sentry-cli bash-hook ? success set -e -_SENTRY_TRACEBACK_FILE="[..].traceback" -_SENTRY_LOG_FILE="[..].out" +_SENTRY_TRACEBACK_FILE='[..].traceback' +_SENTRY_LOG_FILE='[..].out' if [ "${SENTRY_CLI_NO_EXIT_TRAP-0}" != 1 ]; then trap _sentry_exit_trap EXIT @@ -37,7 +37,7 @@ _sentry_err_trap() { echo "@exit_code:${_exit_code}" >> "$_SENTRY_TRACEBACK_FILE" : >> "$_SENTRY_LOG_FILE" - export SENTRY_LAST_EVENT=$([..] bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" --release "0.2.0" --log "$_SENTRY_LOG_FILE" ) + export SENTRY_LAST_EVENT=$([..] bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" --release '0.2.0' --log "$_SENTRY_LOG_FILE" ) rm -f "$_SENTRY_TRACEBACK_FILE" "$_SENTRY_LOG_FILE" } diff --git a/tests/integration/_cases/bash_hook/bash_hook-release.trycmd b/tests/integration/_cases/bash_hook/bash_hook-release.trycmd index 4e2941bac0..093ff9e1f5 100644 --- a/tests/integration/_cases/bash_hook/bash_hook-release.trycmd +++ b/tests/integration/_cases/bash_hook/bash_hook-release.trycmd @@ -3,8 +3,8 @@ $ sentry-cli bash-hook --release "0.1.0" ? success set -e -_SENTRY_TRACEBACK_FILE="[..].traceback" -_SENTRY_LOG_FILE="[..].out" +_SENTRY_TRACEBACK_FILE='[..].traceback' +_SENTRY_LOG_FILE='[..].out' if [ "${SENTRY_CLI_NO_EXIT_TRAP-0}" != 1 ]; then trap _sentry_exit_trap EXIT @@ -37,7 +37,7 @@ _sentry_err_trap() { echo "@exit_code:${_exit_code}" >> "$_SENTRY_TRACEBACK_FILE" : >> "$_SENTRY_LOG_FILE" - export SENTRY_LAST_EVENT=$([..] bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" --release "0.1.0" --log "$_SENTRY_LOG_FILE" ) + export SENTRY_LAST_EVENT=$([..] bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" --release '0.1.0' --log "$_SENTRY_LOG_FILE" ) rm -f "$_SENTRY_TRACEBACK_FILE" "$_SENTRY_LOG_FILE" } diff --git a/tests/integration/_cases/bash_hook/bash_hook-tags.trycmd b/tests/integration/_cases/bash_hook/bash_hook-tags.trycmd index ed0823730b..589a77ab50 100644 --- a/tests/integration/_cases/bash_hook/bash_hook-tags.trycmd +++ b/tests/integration/_cases/bash_hook/bash_hook-tags.trycmd @@ -3,8 +3,8 @@ $ sentry-cli bash-hook --tag "example:value" --tag "example2:value2" ? success set -e -_SENTRY_TRACEBACK_FILE="[..].traceback" -_SENTRY_LOG_FILE="[..].out" +_SENTRY_TRACEBACK_FILE='[..].traceback' +_SENTRY_LOG_FILE='[..].out' if [ "${SENTRY_CLI_NO_EXIT_TRAP-0}" != 1 ]; then trap _sentry_exit_trap EXIT @@ -37,7 +37,7 @@ _sentry_err_trap() { echo "@exit_code:${_exit_code}" >> "$_SENTRY_TRACEBACK_FILE" : >> "$_SENTRY_LOG_FILE" - export SENTRY_LAST_EVENT=$([..] bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" --tag "example:value" --tag "example2:value2" --log "$_SENTRY_LOG_FILE" ) + export SENTRY_LAST_EVENT=$([..] bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" --tag 'example:value' --tag 'example2:value2' --log "$_SENTRY_LOG_FILE" ) rm -f "$_SENTRY_TRACEBACK_FILE" "$_SENTRY_LOG_FILE" } diff --git a/tests/integration/_cases/bash_hook/bash_hook.trycmd b/tests/integration/_cases/bash_hook/bash_hook.trycmd index 2ded28a591..ed074be1ce 100644 --- a/tests/integration/_cases/bash_hook/bash_hook.trycmd +++ b/tests/integration/_cases/bash_hook/bash_hook.trycmd @@ -3,8 +3,8 @@ $ sentry-cli bash-hook ? success set -e -_SENTRY_TRACEBACK_FILE="[..].traceback" -_SENTRY_LOG_FILE="[..].out" +_SENTRY_TRACEBACK_FILE='[..].traceback' +_SENTRY_LOG_FILE='[..].out' if [ "${SENTRY_CLI_NO_EXIT_TRAP-0}" != 1 ]; then trap _sentry_exit_trap EXIT From 1e31ca81d919b63c77b18f85d28b2b9d47732ac2 Mon Sep 17 00:00:00 2001 From: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> Date: Thu, 21 May 2026 11:26:19 +0200 Subject: [PATCH 2/7] fix(config): stricter verify_ssl check Only disable SSL verification when the relevant field is defined in the config and case-insensitively equal to `"false"`. Previously, SSL verification was disabled whenever the option was set, but not case-sensitively equal to `"true"`, which could lead to the case where users who set the option to `"True"` having SSL verification unintentionally disabled. #skip-changelog Fixes [SDK-1238](https://linear.app/getsentry/issue/SDK-1238/sentry-cli-strict-string-comparison-for-ssl-verify-setting-silently) --- src/config.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/config.rs b/src/config.rs index fd844f1236..2a450ae05a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -302,12 +302,14 @@ impl Config { } /// Indicates whether SSL verification should be on or off. + /// + /// Parses the `verify_ssl` key fron the `http` section. Returns `false` only when the key + /// equals `"false"` on a case-insensitive basis; returns `true` otherwise. pub fn should_verify_ssl(&self) -> bool { - let val = self.ini.get_from(Some("http"), "verify_ssl"); - match val { - None => true, - Some(val) => val == "true", - } + self.ini + .get_from(Some("http"), "verify_ssl") + .map(|val| !val.eq_ignore_ascii_case("false")) + .unwrap_or(true) } /// Controls the SSL revocation check on windows. This can be used as a From dc2c943a1907ef7ca5185615a16701cfc39dfa68 Mon Sep 17 00:00:00 2001 From: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> Date: Thu, 21 May 2026 11:26:39 +0200 Subject: [PATCH 3/7] feat(update): Check checksum when updating In hindsight, not sure this change is worth the added complexity. But I suppose it is a bit safer if we check the checksums after download. #skip-changelog Fixes [SDK-1235](https://linear.app/getsentry/issue/SDK-1235/sentry-cli-self-update-binary-downloaded-without-integrity) --- .zed/settings.json | 12 +++---- src/api/mod.rs | 12 ++++--- src/api/updating.rs | 80 +++++++++++++++++++++++++++++++++++++++++++++ src/utils/update.rs | 33 ++++++++++++++++--- 4 files changed, 121 insertions(+), 16 deletions(-) create mode 100644 src/api/updating.rs diff --git a/.zed/settings.json b/.zed/settings.json index 20a2c1fb59..740b73aefb 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -3,12 +3,10 @@ "rust-analyzer": { "initialization_options": { "check": { - "command": "clippy" + "command": "clippy", }, - "cargo": { - "allFeatures": true - } - } - } - } + "cargo": {}, + }, + }, + }, } diff --git a/src/api/mod.rs b/src/api/mod.rs index b53e034953..ed01b96353 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -11,6 +11,7 @@ mod encoding; mod errors; mod pagination; mod serialization; +mod updating; use std::borrow::Cow; use std::cell::RefCell; @@ -57,6 +58,7 @@ use encoding::{PathArg, QueryArg}; use errors::{ApiError, ApiErrorKind, ApiResult, SentryError}; pub use self::data_types::*; +pub use crate::api::updating::ReleaseRegistryFile; lazy_static! { static ref API: Mutex>> = Mutex::new(None); @@ -335,13 +337,13 @@ impl Api { if resp.status() == 200 { let info: RegistryRelease = resp.convert()?; - for (filename, _download_url) in info.file_urls { + for (filename, _download_url) in info.files { info!("Found asset {filename}"); if filename == ref_name { return Ok(Some(SentryCliRelease { version: info.version, #[cfg(not(feature = "managed"))] - download_url: _download_url, + download_info: _download_url, })); } } @@ -1729,17 +1731,17 @@ pub struct ReleaseCommit { pub id: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Deserialize)] struct RegistryRelease { version: String, - file_urls: HashMap, + files: HashMap, } /// Information about sentry CLI releases pub struct SentryCliRelease { pub version: String, #[cfg(not(feature = "managed"))] - pub download_url: String, + pub download_info: ReleaseRegistryFile, } #[derive(Debug, Deserialize, Default)] diff --git a/src/api/updating.rs b/src/api/updating.rs new file mode 100644 index 0000000000..a670f53271 --- /dev/null +++ b/src/api/updating.rs @@ -0,0 +1,80 @@ +//! Types for updating functionality. + +use std::ops::Deref; +use std::str::FromStr; + +use anyhow::{Context as _, Error}; +use serde::de::Error as _; +use serde::{Deserialize, Deserializer}; + +/// A SHA-256 sum in hexadecimal representation is 64 characters long. +const SHA256_SUM_HEX_LENGTH: usize = 64; + +#[derive(Debug)] +pub struct Sha256Sum([u8; 32]); + +#[derive(Debug, Deserialize)] +pub struct ReleaseRegistryFile { + pub url: String, + #[serde(rename = "checksums")] + #[serde(deserialize_with = "deserialize_checksums")] + pub checksum: Sha256Sum, +} + +fn deserialize_checksums<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(rename_all = "kebab-case")] + struct RawChecksumsMapping { + sha256_hex: String, + } + + let RawChecksumsMapping { sha256_hex } = RawChecksumsMapping::deserialize(deserializer)?; + sha256_hex.parse().map_err(D::Error::custom) +} + +impl FromStr for Sha256Sum { + type Err = Error; + + fn from_str(s: &str) -> Result { + if s.len() != SHA256_SUM_HEX_LENGTH { + anyhow::bail!( + "cannot parse SHA-256: expected a {SHA256_SUM_HEX_LENGTH}-character long string" + ); + } + + let mut bytes = [0u8; 32]; + + bytes + .iter_mut() + .zip(s.as_bytes().chunks(2)) + .map(|(byte, hex_byte)| { + let hex_str = str::from_utf8(hex_byte)?; + *byte = u8::from_str_radix(hex_str, 16)?; + Ok::<_, Self::Err>(()) + }) + .map(|result| result.context("cannot parse SHA-256: not a valid hex string")) + .collect::, _>>()?; + + Ok(Sha256Sum(bytes)) + } +} + +impl Deref for Sha256Sum { + type Target = [u8; 32]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PartialEq for Sha256Sum +where + Rhs: Deref, +{ + fn eq(&self, other: &Rhs) -> bool { + self.0 == **other + } +} diff --git a/src/utils/update.rs b/src/utils/update.rs index 345bf6d84e..60aab0bc38 100644 --- a/src/utils/update.rs +++ b/src/utils/update.rs @@ -18,6 +18,8 @@ use log::{debug, info}; use semver::Version; use serde::{Deserialize, Serialize}; +#[cfg(not(feature = "managed"))] +use crate::api::ReleaseRegistryFile; use crate::api::{Api, SentryCliRelease}; use crate::config::Config; use crate::constants::{APP_NAME, VERSION}; @@ -148,9 +150,9 @@ impl SentryCliUpdateInfo { } #[cfg(not(feature = "managed"))] - pub fn download_url(&self) -> Result<&str> { + pub fn download_info(&self) -> Result<&ReleaseRegistryFile> { if let Some(ref rel) = self.latest_release { - Ok(rel.download_url.as_str()) + Ok(&rel.download_info) } else { bail!("Could not get download URL for latest release."); } @@ -158,6 +160,11 @@ impl SentryCliUpdateInfo { #[cfg(not(feature = "managed"))] pub fn download(&self) -> Result<()> { + use std::fs::OpenOptions; + use std::io::{Seek as _, SeekFrom}; + + use sha2::{Digest as _, Sha256}; + let exe = env::current_exe()?; let elevate = !is_writable(&exe); info!("expecting elevation for update: {elevate}"); @@ -166,15 +173,33 @@ impl SentryCliUpdateInfo { } else { exe.parent().unwrap().join(".sentry-cli.part") }; - let mut f = fs::File::create(&tmp_path)?; + let mut f = OpenOptions::new() + .create(true) + .write(true) + .read(true) + .truncate(true) + .open(&tmp_path)?; + let api = Api::current(); - match api.download_with_progress(self.download_url()?, &mut f) { + let ReleaseRegistryFile { url, checksum } = self.download_info()?; + match api.download_with_progress(url, &mut f) { Ok(_) => {} Err(err) => { fs::remove_file(tmp_path).ok(); bail!(err); } }; + f.flush()?; + + f.seek(SeekFrom::Start(0))?; + let mut hasher = Sha256::new(); + io::copy(&mut f, &mut hasher)?; + let hash = hasher.finalize(); + + if *checksum != hash { + fs::remove_file(tmp_path).ok(); + bail!("checksum mismatch"); + } #[cfg(not(windows))] set_executable_mode(&tmp_path)?; From 36d24c2a3f9de3dd8efa060aebda5d860d43d257 Mon Sep 17 00:00:00 2001 From: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> Date: Thu, 21 May 2026 11:26:50 +0200 Subject: [PATCH 4/7] fix(bash-hook): Stop sending environment variables #skip-changelog Fixes [SDK-1240](https://linear.app/getsentry/issue/SDK-1240/sentry-cli-default-environment-variable-disclosure-in-bash-error) --- CHANGELOG.md | 1 + src/bashsupport.sh | 2 +- src/commands/bash_hook.rs | 20 +++---------------- .../_cases/bash_hook/bash_hook-help.trycmd | 1 - .../bash_hook/bash_hook-release-env.trycmd | 3 +-- .../_cases/bash_hook/bash_hook-release.trycmd | 3 +-- .../_cases/bash_hook/bash_hook-tags.trycmd | 3 +-- .../_cases/bash_hook/bash_hook.trycmd | 2 +- 8 files changed, 9 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bcd4c700c..66a20914bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixes - (snapshots) Reject snapshot uploads that have a PR number but no base SHA, since comparisons cannot work without a base reference ([#3300](https://github.com/getsentry/sentry-cli/pull/3300)) +- (bash-hook) We no longer send environment variables in `sentry-cli bash-hook`. ## 3.4.2 diff --git a/src/bashsupport.sh b/src/bashsupport.sh index 36319a8e43..0421eba888 100644 --- a/src/bashsupport.sh +++ b/src/bashsupport.sh @@ -32,7 +32,7 @@ _sentry_err_trap() { echo "@exit_code:${_exit_code}" >> "$_SENTRY_TRACEBACK_FILE" : >> "$_SENTRY_LOG_FILE" - export SENTRY_LAST_EVENT=$(___SENTRY_CLI___ bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" ___SENTRY_TAGS___ ___SENTRY_RELEASE___ --log "$_SENTRY_LOG_FILE" ___SENTRY_NO_ENVIRON___) + export SENTRY_LAST_EVENT=$(___SENTRY_CLI___ bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" ___SENTRY_TAGS___ ___SENTRY_RELEASE___ --log "$_SENTRY_LOG_FILE") rm -f "$_SENTRY_TRACEBACK_FILE" "$_SENTRY_LOG_FILE" } diff --git a/src/commands/bash_hook.rs b/src/commands/bash_hook.rs index 125e00b8c4..88cbfb6250 100644 --- a/src/commands/bash_hook.rs +++ b/src/commands/bash_hook.rs @@ -11,7 +11,7 @@ use anyhow::{format_err, Result}; use clap::{builder::ArgPredicate, Arg, ArgAction, ArgMatches, Command}; use lazy_static::lazy_static; use regex::Regex; -use sentry::protocol::{Event, Exception, Frame, Stacktrace, User, Value}; +use sentry::protocol::{Event, Exception, Frame, Stacktrace, User}; use uuid::Uuid; use crate::commands::send_event; @@ -40,8 +40,9 @@ pub fn make_command(command: Command) -> Command { .arg( Arg::new("no_environ") .long("no-environ") + .hide(true) .action(ArgAction::SetTrue) - .help("Do not send environment variables along"), + .help("No-op, as we never send envrionment variables."), ) .arg( Arg::new("cli") @@ -87,7 +88,6 @@ fn send_event( logfile: &str, tags: &[&String], release: Option, - environ: bool, ) -> Result<()> { let config = Config::current(); @@ -112,13 +112,6 @@ fn send_event( event.tags.insert(key.into(), value.into()); } - if environ { - event.extra.insert( - "environ".into(), - Value::Object(env::vars().map(|(k, v)| (k, Value::String(v))).collect()), - ); - } - let mut cmd = "unknown".to_owned(); let mut exit_code = 1; let mut frames = vec![]; @@ -226,7 +219,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { matches.get_one::("log").unwrap(), &tags, release, - !matches.get_flag("no_environ"), ); } @@ -271,12 +263,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { )), ); - if matches.get_flag("no_environ") { - script = script.replace("___SENTRY_NO_ENVIRON___", "--no-environ"); - } else { - script = script.replace("___SENTRY_NO_ENVIRON___", ""); - } - if !matches.get_flag("no_exit") { script.insert_str(0, "set -e\n\n"); } diff --git a/tests/integration/_cases/bash_hook/bash_hook-help.trycmd b/tests/integration/_cases/bash_hook/bash_hook-help.trycmd index 2f618096fc..479ef1c70d 100644 --- a/tests/integration/_cases/bash_hook/bash_hook-help.trycmd +++ b/tests/integration/_cases/bash_hook/bash_hook-help.trycmd @@ -9,7 +9,6 @@ Options: --no-exit Do not turn on -e (exit immediately) flag automatically --header Custom headers that should be attached to all requests in key:value format. - --no-environ Do not send environment variables along --auth-token Use the given Sentry auth token. --cli Explicitly set/override the sentry-cli command --log-level Set the log output verbosity. [possible values: trace, debug, info, diff --git a/tests/integration/_cases/bash_hook/bash_hook-release-env.trycmd b/tests/integration/_cases/bash_hook/bash_hook-release-env.trycmd index 60188b8786..c793ab53f6 100644 --- a/tests/integration/_cases/bash_hook/bash_hook-release-env.trycmd +++ b/tests/integration/_cases/bash_hook/bash_hook-release-env.trycmd @@ -37,7 +37,7 @@ _sentry_err_trap() { echo "@exit_code:${_exit_code}" >> "$_SENTRY_TRACEBACK_FILE" : >> "$_SENTRY_LOG_FILE" - export SENTRY_LAST_EVENT=$([..] bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" --release '0.2.0' --log "$_SENTRY_LOG_FILE" ) + export SENTRY_LAST_EVENT=$([..] bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" --release '0.2.0' --log "$_SENTRY_LOG_FILE") rm -f "$_SENTRY_TRACEBACK_FILE" "$_SENTRY_LOG_FILE" } @@ -72,4 +72,3 @@ fi ``` - diff --git a/tests/integration/_cases/bash_hook/bash_hook-release.trycmd b/tests/integration/_cases/bash_hook/bash_hook-release.trycmd index 093ff9e1f5..f8997b005f 100644 --- a/tests/integration/_cases/bash_hook/bash_hook-release.trycmd +++ b/tests/integration/_cases/bash_hook/bash_hook-release.trycmd @@ -37,7 +37,7 @@ _sentry_err_trap() { echo "@exit_code:${_exit_code}" >> "$_SENTRY_TRACEBACK_FILE" : >> "$_SENTRY_LOG_FILE" - export SENTRY_LAST_EVENT=$([..] bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" --release '0.1.0' --log "$_SENTRY_LOG_FILE" ) + export SENTRY_LAST_EVENT=$([..] bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" --release '0.1.0' --log "$_SENTRY_LOG_FILE") rm -f "$_SENTRY_TRACEBACK_FILE" "$_SENTRY_LOG_FILE" } @@ -72,4 +72,3 @@ fi ``` - diff --git a/tests/integration/_cases/bash_hook/bash_hook-tags.trycmd b/tests/integration/_cases/bash_hook/bash_hook-tags.trycmd index 589a77ab50..ba4d7a7335 100644 --- a/tests/integration/_cases/bash_hook/bash_hook-tags.trycmd +++ b/tests/integration/_cases/bash_hook/bash_hook-tags.trycmd @@ -37,7 +37,7 @@ _sentry_err_trap() { echo "@exit_code:${_exit_code}" >> "$_SENTRY_TRACEBACK_FILE" : >> "$_SENTRY_LOG_FILE" - export SENTRY_LAST_EVENT=$([..] bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" --tag 'example:value' --tag 'example2:value2' --log "$_SENTRY_LOG_FILE" ) + export SENTRY_LAST_EVENT=$([..] bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" --tag 'example:value' --tag 'example2:value2' --log "$_SENTRY_LOG_FILE") rm -f "$_SENTRY_TRACEBACK_FILE" "$_SENTRY_LOG_FILE" } @@ -72,4 +72,3 @@ fi ``` - diff --git a/tests/integration/_cases/bash_hook/bash_hook.trycmd b/tests/integration/_cases/bash_hook/bash_hook.trycmd index ed074be1ce..7dbe32316d 100644 --- a/tests/integration/_cases/bash_hook/bash_hook.trycmd +++ b/tests/integration/_cases/bash_hook/bash_hook.trycmd @@ -37,7 +37,7 @@ _sentry_err_trap() { echo "@exit_code:${_exit_code}" >> "$_SENTRY_TRACEBACK_FILE" : >> "$_SENTRY_LOG_FILE" - export SENTRY_LAST_EVENT=$([..] bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" --log "$_SENTRY_LOG_FILE" ) + export SENTRY_LAST_EVENT=$([..] bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" --log "$_SENTRY_LOG_FILE") rm -f "$_SENTRY_TRACEBACK_FILE" "$_SENTRY_LOG_FILE" } From 29fe0871d03b15ae0c900e2e9922c9ee9a06dc48 Mon Sep 17 00:00:00 2001 From: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> Date: Thu, 21 May 2026 12:56:20 +0200 Subject: [PATCH 5/7] fix(config): Ensure correct permissions on updated config file Ensure config files have the correct permissions, even when overwriting an existing file, by first creating them as a brand new temporary file, then atomically renaming them over any exisiting file. Fixes [SDK-1234](https://linear.app/getsentry/issue/SDK-1234/sentry-cli-config-file-permissions-not-enforced-on-pre-existing-files) --- src/config.rs | 48 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/src/config.rs b/src/config.rs index 2a450ae05a..efd49deb88 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,7 @@ use std::env; use std::env::VarError; use std::fs; use std::fs::OpenOptions; -use std::io; +use std::io::{self, Write as _}; use std::num::ParseIntError; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -16,6 +16,7 @@ use log::{debug, info, set_max_level, warn}; use parking_lot::Mutex; use secrecy::ExposeSecret as _; use sentry::types::Dsn; +use uuid::Uuid; use crate::constants::CONFIG_INI_FILE_PATH; use crate::constants::DEFAULT_MAX_DIF_ITEM_SIZE; @@ -154,8 +155,17 @@ impl Config { /// Write the current config state back into the file. pub fn save(&self) -> Result<()> { + // Make a unique temp path, containing a random UUID + let temp_path = self + .filename + .clone() + .with_added_extension(Uuid::new_v4().to_string()); + let mut options = OpenOptions::new(); - options.write(true).truncate(true).create(true); + + // Set options so that the file fails to be written if it already exists. It should not + // exist, because the path contains a random UUID. + options.write(true).create_new(true); // Remove all non-user permissions for the newly created file #[cfg(not(windows))] @@ -164,9 +174,17 @@ impl Config { options.mode(0o600); } - let mut file = options.open(&self.filename)?; - self.ini.write_to(&mut file)?; - Ok(()) + { + let mut file = options.open(&temp_path)?; + self.ini.write_to(&mut file).and_then(|()| file.flush()) + // drop file handle + } + .and_then(|()| fs::rename(&temp_path, &self.filename)) + .inspect_err(|_| { + // Best-effort cleanup attempt of the temporary file; errors intentionally ignored. + let _ = fs::remove_file(&temp_path); + }) + .map_err(Into::into) } /// Returns the auth info @@ -773,6 +791,26 @@ mod tests { use super::*; + #[cfg(not(windows))] + #[test] + fn save_restricts_existing_file_permissions() { + use std::os::unix::fs::PermissionsExt as _; + + let dir = tempfile::tempdir().unwrap(); + let filename = dir.path().join(".sentryclirc"); + fs::write(&filename, "[defaults]\nurl=https://sentry.io/\n").unwrap(); + fs::set_permissions(&filename, fs::Permissions::from_mode(0o644)).unwrap(); + + Config::from_file(filename.clone(), Ini::new()) + .save() + .unwrap(); + + assert_eq!( + fs::metadata(filename).unwrap().permissions().mode() & 0o777, + 0o600 + ); + } + #[test] fn test_get_api_endpoint() { let config = Config { From f58542737eab7d3a769e2f0c10096ef7538028de Mon Sep 17 00:00:00 2001 From: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> Date: Thu, 21 May 2026 12:57:19 +0200 Subject: [PATCH 6/7] fix(xcode): Only preprocess `Info.plist` with opt-in #skip-changelog Fixes [SDK-1194](https://linear.app/getsentry/issue/SDK-1194/sentry-cli-compiler-argument-injection-via-malicious-xcode-project) --- CHANGELOG.md | 1 + src/bashsupport.sh | 2 +- src/commands/bash_hook.rs | 17 ++++- src/commands/react_native/xcode.rs | 9 ++- src/commands/releases/propose_version.rs | 12 ++- src/commands/send_event.rs | 11 ++- src/utils/args.rs | 10 +++ src/utils/releases.rs | 16 ++-- src/utils/xcode.rs | 29 ++++++-- ...allow-xcode-infoplist-preprocessing.trycmd | 74 +++++++++++++++++++ .../_cases/bash_hook/bash_hook-help.trycmd | 36 +++++---- .../_cases/react_native/xcode-help.trycmd | 61 +++++++++++++++ .../releases-propose-version-help.trycmd | 29 ++++++++ .../_cases/send_event/send_event-help.trycmd | 4 + tests/integration/react_native/xcode.rs | 5 ++ tests/integration/releases/mod.rs | 5 ++ 16 files changed, 285 insertions(+), 36 deletions(-) create mode 100644 tests/integration/_cases/bash_hook/bash_hook-allow-xcode-infoplist-preprocessing.trycmd create mode 100644 tests/integration/_cases/react_native/xcode-help.trycmd create mode 100644 tests/integration/_cases/releases/releases-propose-version-help.trycmd diff --git a/CHANGELOG.md b/CHANGELOG.md index 66a20914bf..17e99f20fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixes +- **Behavior-breaking**: Disable Xcode `Info.plist` preprocessing by default to avoid passing project-controlled compiler settings to `cc` during release auto-discovery. This affects `sentry-cli releases propose-version`, `sentry-cli send-event` and `sentry-cli bash-hook --send-event` release inference, and `sentry-cli react-native xcode` auto-release detection. Use `--allow-xcode-infoplist-preprocessing` only for trusted projects that require preprocessing. - (snapshots) Reject snapshot uploads that have a PR number but no base SHA, since comparisons cannot work without a base reference ([#3300](https://github.com/getsentry/sentry-cli/pull/3300)) - (bash-hook) We no longer send environment variables in `sentry-cli bash-hook`. diff --git a/src/bashsupport.sh b/src/bashsupport.sh index 0421eba888..6920f4d61a 100644 --- a/src/bashsupport.sh +++ b/src/bashsupport.sh @@ -32,7 +32,7 @@ _sentry_err_trap() { echo "@exit_code:${_exit_code}" >> "$_SENTRY_TRACEBACK_FILE" : >> "$_SENTRY_LOG_FILE" - export SENTRY_LAST_EVENT=$(___SENTRY_CLI___ bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" ___SENTRY_TAGS___ ___SENTRY_RELEASE___ --log "$_SENTRY_LOG_FILE") + export SENTRY_LAST_EVENT=$(___SENTRY_CLI___ bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" ___SENTRY_TAGS___ ___SENTRY_RELEASE___ ___SENTRY_ALLOW_XCODE_INFOPLIST_PREPROCESSING___ --log "$_SENTRY_LOG_FILE") rm -f "$_SENTRY_TRACEBACK_FILE" "$_SENTRY_LOG_FILE" } diff --git a/src/commands/bash_hook.rs b/src/commands/bash_hook.rs index 88cbfb6250..4f1cac517e 100644 --- a/src/commands/bash_hook.rs +++ b/src/commands/bash_hook.rs @@ -16,6 +16,7 @@ use uuid::Uuid; use crate::commands::send_event; use crate::config::Config; +use crate::utils::args::allow_xcode_infoplist_preprocessing_arg; use crate::utils::event::{attach_logfile, get_sdk_info}; use crate::utils::releases::detect_release_name; @@ -50,6 +51,7 @@ pub fn make_command(command: Command) -> Command { .value_name("CMD") .help("Explicitly set/override the sentry-cli command"), ) + .arg(allow_xcode_infoplist_preprocessing_arg()) .arg( Arg::new("send_event") .long("send-event") @@ -88,12 +90,15 @@ fn send_event( logfile: &str, tags: &[&String], release: Option, + allow_xcode_infoplist_preprocessing: bool, ) -> Result<()> { let config = Config::current(); let mut event = Event { environment: config.get_environment().map(Into::into), - release: release.or(detect_release_name().ok()).map(Into::into), + release: release + .or(detect_release_name(allow_xcode_infoplist_preprocessing).ok()) + .map(Into::into), sdk: Some(get_sdk_info()), user: whoami::fallible::username().ok().map(|n| User { username: Some(n), @@ -219,6 +224,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { matches.get_one::("log").unwrap(), &tags, release, + matches.get_flag("allow_xcode_infoplist_preprocessing"), ); } @@ -263,6 +269,15 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { )), ); + if matches.get_flag("allow_xcode_infoplist_preprocessing") { + script = script.replace( + " ___SENTRY_ALLOW_XCODE_INFOPLIST_PREPROCESSING___", + " --allow-xcode-infoplist-preprocessing", + ); + } else { + script = script.replace(" ___SENTRY_ALLOW_XCODE_INFOPLIST_PREPROCESSING___", ""); + } + if !matches.get_flag("no_exit") { script.insert_str(0, "set -e\n\n"); } diff --git a/src/commands/react_native/xcode.rs b/src/commands/react_native/xcode.rs index 4ff365f34e..653c2a3d08 100644 --- a/src/commands/react_native/xcode.rs +++ b/src/commands/react_native/xcode.rs @@ -18,7 +18,9 @@ use serde_json::Value; use crate::api::Api; use crate::config::Config; use crate::constants::DEFAULT_MAX_WAIT; -use crate::utils::args::{validate_distribution, ArgExt as _}; +use crate::utils::args::{ + allow_xcode_infoplist_preprocessing_arg, validate_distribution, ArgExt as _, +}; use crate::utils::file_search::ReleaseFileSearch; use crate::utils::file_upload::UploadContext; use crate::utils::fs::TempFile; @@ -122,6 +124,7 @@ pub fn make_command(command: Command) -> Command { but at most for the given number of seconds.", ), ) + .arg(allow_xcode_infoplist_preprocessing_arg()) .arg( Arg::new("no_auto_release") .long("no-auto-release") @@ -358,7 +361,9 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { (Err(_), Err(_)) => { // Neither environment variable is present, attempt to parse Info.plist info!("Parsing Info.plist"); - match InfoPlist::discover_from_env() { + match InfoPlist::discover_from_env( + matches.get_flag("allow_xcode_infoplist_preprocessing"), + ) { Ok(Some(plist)) => { // Successfully discovered and parsed Info.plist let dist_string = plist.build().to_owned(); diff --git a/src/commands/releases/propose_version.rs b/src/commands/releases/propose_version.rs index 56e8826f3a..a2198334ea 100644 --- a/src/commands/releases/propose_version.rs +++ b/src/commands/releases/propose_version.rs @@ -1,13 +1,19 @@ use anyhow::Result; use clap::{ArgMatches, Command}; +use crate::utils::args::allow_xcode_infoplist_preprocessing_arg; use crate::utils::releases::detect_release_name; pub fn make_command(command: Command) -> Command { - command.about("Propose a version name for a new release.") + command + .about("Propose a version name for a new release.") + .arg(allow_xcode_infoplist_preprocessing_arg()) } -pub fn execute(_matches: &ArgMatches) -> Result<()> { - println!("{}", detect_release_name()?); +pub fn execute(matches: &ArgMatches) -> Result<()> { + println!( + "{}", + detect_release_name(matches.get_flag("allow_xcode_infoplist_preprocessing"))? + ); Ok(()) } diff --git a/src/commands/send_event.rs b/src/commands/send_event.rs index 9e166b7fd0..b53f5b8a16 100644 --- a/src/commands/send_event.rs +++ b/src/commands/send_event.rs @@ -16,7 +16,9 @@ use serde_json::Value; use crate::api::envelopes_api::EnvelopesApi; use crate::constants::USER_AGENT; -use crate::utils::args::{get_timestamp, validate_distribution}; +use crate::utils::args::{ + allow_xcode_infoplist_preprocessing_arg, get_timestamp, validate_distribution, +}; use crate::utils::event::{attach_logfile, get_sdk_info}; use crate::utils::releases::detect_release_name; @@ -148,6 +150,7 @@ pub fn make_command(command: Command) -> Command { .long("logfile") .help(format!("Send a logfile as breadcrumbs with the event (last {MAX_BREADCRUMBS} records)")), ) + .arg(allow_xcode_infoplist_preprocessing_arg()) .arg( Arg::new("with_categories") .long("with-categories") @@ -231,7 +234,11 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { release: matches .get_one::("release") .map(|s| Cow::Owned(s.clone())) - .or_else(|| detect_release_name().ok().map(Cow::from)), + .or_else(|| { + detect_release_name(matches.get_flag("allow_xcode_infoplist_preprocessing")) + .ok() + .map(Cow::from) + }), dist: matches .get_one::("dist") .map(|s| Cow::Owned(s.clone())), diff --git a/src/utils/args.rs b/src/utils/args.rs index de983da7be..a119c9e167 100644 --- a/src/utils/args.rs +++ b/src/utils/args.rs @@ -68,6 +68,16 @@ pub fn validate_distribution(v: &str) -> Result { } } +pub fn allow_xcode_infoplist_preprocessing_arg() -> Arg { + Arg::new("allow_xcode_infoplist_preprocessing") + .long("allow-xcode-infoplist-preprocessing") + .action(ArgAction::SetTrue) + .help( + "Allow Xcode Info.plist preprocessing with cc. This passes Xcode project settings to \ + the compiler. Do not use with untrusted code!", + ) +} + pub fn get_timestamp(value: &str) -> Result> { if let Ok(int) = value.parse::() { #[expect(clippy::unwrap_used, reason = "legacy code")] diff --git a/src/utils/releases.rs b/src/utils/releases.rs index c87effa5b1..48e2852303 100644 --- a/src/utils/releases.rs +++ b/src/utils/releases.rs @@ -44,10 +44,14 @@ pub fn get_cordova_release_name(path: Option) -> Result> } } -pub fn get_xcode_release_name(plist: Option) -> Result> { - // if we are executed from within xcode, then we can use the environment - // based discovery to get a release name without any interpolation. - if let Some(plist) = plist.or(InfoPlist::discover_from_env()?) { +pub fn get_xcode_release_name( + plist: Option, + allow_xcode_infoplist_preprocessing: bool, +) -> Result> { + // If we are executed from within Xcode, use environment-based discovery. + if let Some(plist) = plist.or(InfoPlist::discover_from_env( + allow_xcode_infoplist_preprocessing, + )?) { return Ok(Some(plist.get_release_name())); } @@ -86,7 +90,7 @@ pub fn infer_gradle_release_name(path: Option) -> Result } /// Detects the release name for the current working directory. -pub fn detect_release_name() -> Result { +pub fn detect_release_name(allow_xcode_infoplist_preprocessing: bool) -> Result { // try SENTRY_RELEASE environment variable if let Ok(release) = env::var("SENTRY_RELEASE") { if !release.is_empty() { @@ -145,7 +149,7 @@ pub fn detect_release_name() -> Result { // xcodebuild which does not exist anywhere but there. if_chain! { if cfg!(target_os="macos"); - if let Some(release) = get_xcode_release_name(None)?; + if let Some(release) = get_xcode_release_name(None, allow_xcode_infoplist_preprocessing)?; then { return Ok(release) } diff --git a/src/utils/xcode.rs b/src/utils/xcode.rs index 6c62ce5131..641d0a8e19 100644 --- a/src/utils/xcode.rs +++ b/src/utils/xcode.rs @@ -189,15 +189,18 @@ impl XcodeProjectInfo { impl InfoPlist { /// Loads a processed plist file. - pub fn discover_from_env() -> Result> { - // if we are loaded directly from xcode we can trust the os environment - // and pass those variables to the processor. + pub fn discover_from_env(allow_infoplist_preprocessing: bool) -> Result> { + // If we are loaded directly from Xcode, use the environment for plist discovery. if env::var("XCODE_VERSION_ACTUAL").is_ok() { let vars: HashMap<_, _> = env::vars().collect(); if let Some(filename) = vars.get("INFOPLIST_FILE") { let base = vars.get("PROJECT_DIR").map(String::as_str).unwrap_or("."); let path = env::current_dir().unwrap().join(base).join(filename); - Ok(Some(InfoPlist::load_and_process(path, &vars)?)) + Ok(Some(InfoPlist::load_and_process( + path, + &vars, + allow_infoplist_preprocessing, + )?)) } else if let Ok(default_plist) = InfoPlist::from_env_vars(&vars) { Ok(Some(default_plist)) } else { @@ -212,7 +215,7 @@ impl InfoPlist { if let Ok(here) = env::current_dir(); if let Some(pi) = get_xcode_project_info(&here)?; then { - InfoPlist::from_project_info(&pi) + InfoPlist::from_project_info(&pi, allow_infoplist_preprocessing) } else { Ok(None) } @@ -221,7 +224,10 @@ impl InfoPlist { } /// Loads an info plist from a given project info - pub fn from_project_info(pi: &XcodeProjectInfo) -> Result> { + pub fn from_project_info( + pi: &XcodeProjectInfo, + allow_infoplist_preprocessing: bool, + ) -> Result> { if_chain! { if let Some(config) = pi.get_configuration("release") .or_else(|| pi.get_configuration("debug")); @@ -234,7 +240,11 @@ impl InfoPlist { let base = vars.get("PROJECT_DIR").map(Path::new) .unwrap_or_else(|| pi.base_path()); let path = base.join(path); - return Ok(Some(InfoPlist::load_and_process(path, &vars)?)) + return Ok(Some(InfoPlist::load_and_process( + path, + &vars, + allow_infoplist_preprocessing, + )?)) } } } @@ -245,9 +255,12 @@ impl InfoPlist { pub fn load_and_process>( path: P, vars: &HashMap, + allow_infoplist_preprocessing: bool, ) -> Result { // do we want to preprocess the plist file? - let plist = if vars.get("INFOPLIST_PREPROCESS").map(String::as_str) == Some("YES") { + let should_preprocess = allow_infoplist_preprocessing + && vars.get("INFOPLIST_PREPROCESS").map(String::as_str) == Some("YES"); + let plist = if should_preprocess { let mut c = process::Command::new("cc"); c.arg("-xc").arg("-P").arg("-E"); if let Some(defs) = vars.get("INFOPLIST_OTHER_PREPROCESSOR_FLAGS") { diff --git a/tests/integration/_cases/bash_hook/bash_hook-allow-xcode-infoplist-preprocessing.trycmd b/tests/integration/_cases/bash_hook/bash_hook-allow-xcode-infoplist-preprocessing.trycmd new file mode 100644 index 0000000000..19e6171569 --- /dev/null +++ b/tests/integration/_cases/bash_hook/bash_hook-allow-xcode-infoplist-preprocessing.trycmd @@ -0,0 +1,74 @@ +``` +$ sentry-cli bash-hook --allow-xcode-infoplist-preprocessing +? success +set -e + +_SENTRY_TRACEBACK_FILE='[..].traceback' +_SENTRY_LOG_FILE='[..].out' + +[..] + trap _sentry_exit_trap EXIT +fi +trap _sentry_err_trap ERR + +_sentry_shown_traceback=0 + +_sentry_exit_trap() { + local _exit_code="$?" + local _command="${BASH_COMMAND:-unknown}" + if [[ $_exit_code != 0 && "${_sentry_shown_traceback}" != 1 ]]; then + _sentry_err_trap "$_command" "$_exit_code" + fi + rm -f "$_SENTRY_TRACEBACK_FILE" "$_SENTRY_LOG_FILE" + exit $_exit_code +} + +_sentry_err_trap() { + local _exit_code="$?" + local _command="${BASH_COMMAND:-unknown}" + if [ $# -ge 1 ] && [ "x$1" != x ]; then + _command="$1" + fi + if [ $# -ge 2 ] && [ "x$2" != x ]; then + _exit_code="$2" + fi + _sentry_traceback 1 + echo "@command:${_command}" >> "$_SENTRY_TRACEBACK_FILE" + echo "@exit_code:${_exit_code}" >> "$_SENTRY_TRACEBACK_FILE" + + : >> "$_SENTRY_LOG_FILE" + export SENTRY_LAST_EVENT=$([..] bash-hook --send-event --traceback "$_SENTRY_TRACEBACK_FILE" --allow-xcode-infoplist-preprocessing --log "$_SENTRY_LOG_FILE") + rm -f "$_SENTRY_TRACEBACK_FILE" "$_SENTRY_LOG_FILE" +} + +_sentry_traceback() { + _sentry_shown_traceback=1 + local -i start=$(( ${1:-0} + 1 )) + local -i end=${#BASH_SOURCE[@]} + local -i i=0 + local -i j=0 + + : > "$_SENTRY_TRACEBACK_FILE" + for ((i=${start}; i < ${end}; i++)); do + j=$(( $i - 1 )) + local function="${FUNCNAME[$i]}" + local file="${BASH_SOURCE[$i]}" + local line="${BASH_LINENO[$j]}" + echo "${function}:${file}:${line}" >> "$_SENTRY_TRACEBACK_FILE" + done +} + +: > "$_SENTRY_LOG_FILE" + +if command -v perl >/dev/null; then + exec / + 1> >(tee >(perl '-MPOSIX' -ne '$|++; print strftime("%Y-%m-%d %H:%M:%S %z: ", localtime()), "stdout: ", $_;' >> "$_SENTRY_LOG_FILE")) / + 2> >(tee >(perl '-MPOSIX' -ne '$|++; print strftime("%Y-%m-%d %H:%M:%S %z: ", localtime()), "stderr: ", $_;' >> "$_SENTRY_LOG_FILE") >&2) +else + exec / + 1> >(tee >(awk '{ system(""); print strftime("%Y-%m-%d %H:%M:%S %z:"), "stdout:", $0; system(""); }' >> "$_SENTRY_LOG_FILE")) / + 2> >(tee >(awk '{ system(""); print strftime("%Y-%m-%d %H:%M:%S %z:"), "stderr:", $0; system(""); }' >> "$_SENTRY_LOG_FILE") >&2) +fi + + +``` diff --git a/tests/integration/_cases/bash_hook/bash_hook-help.trycmd b/tests/integration/_cases/bash_hook/bash_hook-help.trycmd index 479ef1c70d..472eeb07ac 100644 --- a/tests/integration/_cases/bash_hook/bash_hook-help.trycmd +++ b/tests/integration/_cases/bash_hook/bash_hook-help.trycmd @@ -6,18 +6,28 @@ Prints out a bash script that does error handling. Usage: sentry-cli[EXE] bash-hook [OPTIONS] Options: - --no-exit Do not turn on -e (exit immediately) flag automatically - --header Custom headers that should be attached to all requests - in key:value format. - --auth-token Use the given Sentry auth token. - --cli Explicitly set/override the sentry-cli command - --log-level Set the log output verbosity. [possible values: trace, debug, info, - warn, error] - --quiet Do not print any output while preserving correct exit code. This - flag is currently implemented only for selected subcommands. - [aliases: --silent] - --tag Add tags (key:value) to the event. - --release Define release version for the event. - -h, --help Print help + --no-exit + Do not turn on -e (exit immediately) flag automatically + --header + Custom headers that should be attached to all requests + in key:value format. + --auth-token + Use the given Sentry auth token. + --cli + Explicitly set/override the sentry-cli command + --allow-xcode-infoplist-preprocessing + Allow Xcode Info.plist preprocessing with cc. This passes Xcode project settings to the + compiler. Do not use with untrusted code! + --log-level + Set the log output verbosity. [possible values: trace, debug, info, warn, error] + --quiet + Do not print any output while preserving correct exit code. This flag is currently + implemented only for selected subcommands. [aliases: --silent] + --tag + Add tags (key:value) to the event. + --release + Define release version for the event. + -h, --help + Print help ``` diff --git a/tests/integration/_cases/react_native/xcode-help.trycmd b/tests/integration/_cases/react_native/xcode-help.trycmd new file mode 100644 index 0000000000..0df0ceee94 --- /dev/null +++ b/tests/integration/_cases/react_native/xcode-help.trycmd @@ -0,0 +1,61 @@ +``` +$ sentry-cli react-native xcode --help +? success +Upload react-native projects in a Xcode build step. + +Usage: sentry-cli react-native xcode [OPTIONS] [BUILD_SCRIPT] [-- ...] + +Arguments: + [BUILD_SCRIPT] Optional path to the build script. + This is the path to the `react-native-xcode.sh` script you want to use. By + default the bundled build script is used. + [ARGS]... Optional arguments to pass to the build script. + +Options: + -o, --org + The organization ID or slug. + --header + Custom headers that should be attached to all requests + in key:value format. + -p, --project + The project ID or slug. + --auth-token + Use the given Sentry auth token. + -f, --force + Force the script to run, even in debug configuration. + This rarely does what you want because the default build script does not actually produce + any information that the sentry build tool could pick up on. + --allow-fetch + Enable sourcemap fetching from the packager. + If this is enabled the react native packager needs to run and sourcemaps are downloade + from it if the simulator platform is detected. + --log-level + Set the log output verbosity. [possible values: trace, debug, info, warn, error] + --fetch-from + Set the URL to fetch sourcemaps from. + The default is http://127.0.0.1:8081/, where the react-native packager runs by default. + --quiet + Do not print any output while preserving correct exit code. This flag is currently + implemented only for selected subcommands. [aliases: --silent] + --force-foreground + Wait for the process to finish. + By default part of the build process will when triggered from Xcode detach and continue in + the background. When an error happens, a dialog is shown. If this parameter is passed, + Xcode will wait for the process to finish before the build finishes and output will be + shown in the Xcode build output. + --dist + The names of the distributions to publish. Can be supplied multiple times. + --wait + Wait for the server to fully process uploaded files. + --wait-for + Wait for the server to fully process uploaded files, but at most for the given number of + seconds. + --allow-xcode-infoplist-preprocessing + Allow Xcode Info.plist preprocessing with cc. This passes Xcode project settings to the + compiler. Do not use with untrusted code! + --no-auto-release + Don't try to automatically read release from Xcode project files. + -h, --help + Print help + +``` diff --git a/tests/integration/_cases/releases/releases-propose-version-help.trycmd b/tests/integration/_cases/releases/releases-propose-version-help.trycmd new file mode 100644 index 0000000000..34ba635cf4 --- /dev/null +++ b/tests/integration/_cases/releases/releases-propose-version-help.trycmd @@ -0,0 +1,29 @@ +``` +$ sentry-cli releases propose-version --help +? success +Propose a version name for a new release. + +Usage: sentry-cli[EXE] releases propose-version [OPTIONS] + +Options: + --allow-xcode-infoplist-preprocessing + Allow Xcode Info.plist preprocessing with cc. This passes Xcode project settings to the + compiler. Do not use with untrusted code! + -o, --org + The organization ID or slug. + --header + Custom headers that should be attached to all requests + in key:value format. + -p, --project + The project ID or slug. + --auth-token + Use the given Sentry auth token. + --log-level + Set the log output verbosity. [possible values: trace, debug, info, warn, error] + --quiet + Do not print any output while preserving correct exit code. This flag is currently + implemented only for selected subcommands. [aliases: --silent] + -h, --help + Print help + +``` diff --git a/tests/integration/_cases/send_event/send_event-help.trycmd b/tests/integration/_cases/send_event/send_event-help.trycmd index 95755972c0..6545d3716f 100644 --- a/tests/integration/_cases/send_event/send_event-help.trycmd +++ b/tests/integration/_cases/send_event/send_event-help.trycmd @@ -76,6 +76,10 @@ Options: --logfile Send a logfile as breadcrumbs with the event (last 100 records) + --allow-xcode-infoplist-preprocessing + Allow Xcode Info.plist preprocessing with cc. This passes Xcode project settings to the + compiler. Do not use with untrusted code! + --with-categories When logfile is provided, this flag will try to assign correct level to extracted log breadcrumbs. It uses standard log format of "category: message". eg. "INFO: Something diff --git a/tests/integration/react_native/xcode.rs b/tests/integration/react_native/xcode.rs index 60b2abebe9..62d15b75b8 100644 --- a/tests/integration/react_native/xcode.rs +++ b/tests/integration/react_native/xcode.rs @@ -1,5 +1,10 @@ use crate::integration::TestManager; +#[test] +fn xcode_help() { + TestManager::new().register_trycmd_test("react_native/xcode-help.trycmd"); +} + #[test] fn xcode_upload_source_maps_missing_plist() { TestManager::new() diff --git a/tests/integration/releases/mod.rs b/tests/integration/releases/mod.rs index 25c8092403..24bde918a9 100644 --- a/tests/integration/releases/mod.rs +++ b/tests/integration/releases/mod.rs @@ -15,3 +15,8 @@ fn command_releases_help() { fn command_releases_no_subcommand() { TestManager::new().register_trycmd_test("releases/releases-no-subcommand.trycmd"); } + +#[test] +fn command_releases_propose_version_help() { + TestManager::new().register_trycmd_test("releases/releases-propose-version-help.trycmd"); +} From e34427617ce76fc5a105a0b7ca0d8fdddcbea3dc Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Thu, 21 May 2026 14:18:06 +0200 Subject: [PATCH 7/7] meta(changelog): Update/add security fix changelog entries --- CHANGELOG.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17e99f20fd..5b179c642b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,15 +2,22 @@ ## Unreleased +### Security Fixes + +- **Behavior-breaking**: Disable Xcode `Info.plist` preprocessing by default to avoid passing project-controlled compiler settings to `cc` during release auto-discovery. This affects `sentry-cli releases propose-version`, `sentry-cli send-event` and `sentry-cli bash-hook --send-event` release inference, and `sentry-cli react-native xcode` auto-release detection. Use `--allow-xcode-infoplist-preprocessing` only for trusted projects that require preprocessing. +- Ensure restrictive file permissions maintained when `sentry-cli login` updates existing config files. +- Disable TLS verification only when `http.verify_ssl` is set to `false`, case-insensitively. +- Shell-escape generated `bash-hook` arguments, including paths, tags, release names, and the CLI path. +- Stop sending environment variables in `sentry-cli bash-hook` events. +- Verify the downloaded binary checksum before replacing the current executable in `sentry-cli update`. + ### Performance - (snapshots) Skip uploading images that already exist in objectstore by batch-checking with HEAD requests first ([#3305](https://github.com/getsentry/sentry-cli/pull/3305)) ### Fixes -- **Behavior-breaking**: Disable Xcode `Info.plist` preprocessing by default to avoid passing project-controlled compiler settings to `cc` during release auto-discovery. This affects `sentry-cli releases propose-version`, `sentry-cli send-event` and `sentry-cli bash-hook --send-event` release inference, and `sentry-cli react-native xcode` auto-release detection. Use `--allow-xcode-infoplist-preprocessing` only for trusted projects that require preprocessing. - (snapshots) Reject snapshot uploads that have a PR number but no base SHA, since comparisons cannot work without a base reference ([#3300](https://github.com/getsentry/sentry-cli/pull/3300)) -- (bash-hook) We no longer send environment variables in `sentry-cli bash-hook`. ## 3.4.2