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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 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`.

## 2.58.5

### Fixes
Expand Down
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ secrecy = "0.8.0"
lru = "0.16.0"
backon = { version = "1.5.2", features = ["std", "std-blocking-sleep"] }
brotli = "8.0.2"
sha2 = "0.10.9"

[dev-dependencies]
assert_cmd = "2.0.11"
Expand Down
12 changes: 7 additions & 5 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod data_types;
mod encoding;
mod errors;
mod pagination;
mod updating;

use std::borrow::Cow;
use std::cell::RefCell;
Expand Down Expand Up @@ -62,6 +63,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<Option<Arc<Api>>> = Mutex::new(None);
Expand Down Expand Up @@ -344,13 +346,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,
}));
}
}
Expand Down Expand Up @@ -2199,17 +2201,17 @@ pub struct ReleaseCommit {
pub id: String,
}

#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Deserialize)]
struct RegistryRelease {
version: String,
file_urls: HashMap<String, String>,
files: HashMap<String, ReleaseRegistryFile>,
}

/// 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)]
Expand Down
80 changes: 80 additions & 0 deletions src/api/updating.rs
Original file line number Diff line number Diff line change
@@ -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<Sha256Sum, D::Error>
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<Self, Self::Err> {
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::<Result<Vec<()>, _>>()?;

Ok(Sha256Sum(bytes))
}
}

impl Deref for Sha256Sum {
type Target = [u8; 32];

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<Rhs> PartialEq<Rhs> for Sha256Sum
where
Rhs: Deref<Target = [u8]>,
{
fn eq(&self, other: &Rhs) -> bool {
self.0 == **other
}
}
6 changes: 3 additions & 3 deletions src/bashsupport.sh
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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___ ___SENTRY_ALLOW_XCODE_INFOPLIST_PREPROCESSING___ --log "$_SENTRY_LOG_FILE")
rm -f "$_SENTRY_TRACEBACK_FILE" "$_SENTRY_LOG_FILE"
}

Expand Down
70 changes: 44 additions & 26 deletions src/commands/bash_hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ 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;
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;

Expand All @@ -40,15 +41,17 @@ 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")
.long("cli")
.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")
Expand Down Expand Up @@ -87,13 +90,15 @@ fn send_event(
logfile: &str,
tags: &[&String],
release: Option<String>,
environ: bool,
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),
Expand All @@ -112,13 +117,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![];
Expand Down Expand Up @@ -208,6 +206,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();

Expand All @@ -222,7 +224,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
matches.get_one::<String>("log").unwrap(),
&tags,
release,
!matches.get_flag("no_environ"),
matches.get_flag("allow_xcode_infoplist_preprocessing"),
);
}

Expand All @@ -235,42 +237,45 @@ 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::<Vec<_>>()
.join(""),
);

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::<String>("cli")
.map_or_else(
|| env::current_exe().unwrap().display().to_string(),
String::clone,
)
.as_str(),
&shell_quote(&matches.get_one::<String>("cli").map_or_else(
|| env::current_exe().unwrap().display().to_string(),
String::clone,
)),
);

if matches.get_flag("no_environ") {
script = script.replace("___SENTRY_NO_ENVIRON___", "--no-environ");
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_NO_ENVIRON___", "");
script = script.replace(" ___SENTRY_ALLOW_XCODE_INFOPLIST_PREPROCESSING___", "");
}

if !matches.get_flag("no_exit") {
Expand All @@ -279,3 +284,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'"
);
}
}
6 changes: 5 additions & 1 deletion src/commands/react_native/appcenter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ use crate::api::Api;
use crate::config::Config;
use crate::constants::DEFAULT_MAX_WAIT;
use crate::utils::appcenter::{get_appcenter_package, get_react_native_appcenter_release};
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::sourcemaps::SourceMapProcessor;
Expand Down Expand Up @@ -108,6 +110,7 @@ pub fn make_command(command: Command) -> Command {
but at most for the given number of seconds.",
),
)
.arg(allow_xcode_infoplist_preprocessing_arg())
}

pub fn execute(matches: &ArgMatches) -> Result<()> {
Expand Down Expand Up @@ -146,6 +149,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
matches
.get_one::<String>("release_name")
.map(String::as_str),
matches.get_flag("allow_xcode_infoplist_preprocessing"),
)?;
if print_release_name {
println!("{release}");
Expand Down
Loading
Loading