Skip to content

Commit 69f5028

Browse files
fix: Various fixes (#3308)
This PR contains the following commits, which are ported from a private repo - [`e344276`](e344276): changelog update - [`f585427`](f585427): getsentry/sentry-cli-security-fixes#13 - [`29fe087`](29fe087): getsentry/sentry-cli-security-fixes#22 - [`36d24c2`](36d24c2): getsentry/sentry-cli-security-fixes#18 - [`dc2c943`](dc2c943): getsentry/sentry-cli-security-fixes#19 - [`1e31ca8`](1e31ca8): getsentry/sentry-cli-security-fixes#21 - [`769b8ce`](769b8ce): getsentry/sentry-cli-security-fixes#11 #skip-changelog
1 parent 9eabaaa commit 69f5028

25 files changed

Lines changed: 506 additions & 105 deletions

.zed/settings.json

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@
33
"rust-analyzer": {
44
"initialization_options": {
55
"check": {
6-
"command": "clippy"
6+
"command": "clippy",
77
},
8-
"cargo": {
9-
"allFeatures": true
10-
}
11-
}
12-
}
13-
}
8+
"cargo": {},
9+
},
10+
},
11+
},
1412
}

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
## Unreleased
44

5+
### Security Fixes
6+
7+
- **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.
8+
- Ensure restrictive file permissions maintained when `sentry-cli login` updates existing config files.
9+
- Disable TLS verification only when `http.verify_ssl` is set to `false`, case-insensitively.
10+
- Shell-escape generated `bash-hook` arguments, including paths, tags, release names, and the CLI path.
11+
- Stop sending environment variables in `sentry-cli bash-hook` events.
12+
- Verify the downloaded binary checksum before replacing the current executable in `sentry-cli update`.
13+
514
### Performance
615

716
- (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))

src/api/mod.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod encoding;
1111
mod errors;
1212
mod pagination;
1313
mod serialization;
14+
mod updating;
1415

1516
use std::borrow::Cow;
1617
use std::cell::RefCell;
@@ -57,6 +58,7 @@ use encoding::{PathArg, QueryArg};
5758
use errors::{ApiError, ApiErrorKind, ApiResult, SentryError};
5859

5960
pub use self::data_types::*;
61+
pub use crate::api::updating::ReleaseRegistryFile;
6062

6163
lazy_static! {
6264
static ref API: Mutex<Option<Arc<Api>>> = Mutex::new(None);
@@ -335,13 +337,13 @@ impl Api {
335337

336338
if resp.status() == 200 {
337339
let info: RegistryRelease = resp.convert()?;
338-
for (filename, _download_url) in info.file_urls {
340+
for (filename, _download_url) in info.files {
339341
info!("Found asset {filename}");
340342
if filename == ref_name {
341343
return Ok(Some(SentryCliRelease {
342344
version: info.version,
343345
#[cfg(not(feature = "managed"))]
344-
download_url: _download_url,
346+
download_info: _download_url,
345347
}));
346348
}
347349
}
@@ -1729,17 +1731,17 @@ pub struct ReleaseCommit {
17291731
pub id: String,
17301732
}
17311733

1732-
#[derive(Debug, Serialize, Deserialize)]
1734+
#[derive(Debug, Deserialize)]
17331735
struct RegistryRelease {
17341736
version: String,
1735-
file_urls: HashMap<String, String>,
1737+
files: HashMap<String, ReleaseRegistryFile>,
17361738
}
17371739

17381740
/// Information about sentry CLI releases
17391741
pub struct SentryCliRelease {
17401742
pub version: String,
17411743
#[cfg(not(feature = "managed"))]
1742-
pub download_url: String,
1744+
pub download_info: ReleaseRegistryFile,
17431745
}
17441746

17451747
#[derive(Debug, Deserialize, Default)]

src/api/updating.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//! Types for updating functionality.
2+
3+
use std::ops::Deref;
4+
use std::str::FromStr;
5+
6+
use anyhow::{Context as _, Error};
7+
use serde::de::Error as _;
8+
use serde::{Deserialize, Deserializer};
9+
10+
/// A SHA-256 sum in hexadecimal representation is 64 characters long.
11+
const SHA256_SUM_HEX_LENGTH: usize = 64;
12+
13+
#[derive(Debug)]
14+
pub struct Sha256Sum([u8; 32]);
15+
16+
#[derive(Debug, Deserialize)]
17+
pub struct ReleaseRegistryFile {
18+
pub url: String,
19+
#[serde(rename = "checksums")]
20+
#[serde(deserialize_with = "deserialize_checksums")]
21+
pub checksum: Sha256Sum,
22+
}
23+
24+
fn deserialize_checksums<'de, D>(deserializer: D) -> Result<Sha256Sum, D::Error>
25+
where
26+
D: Deserializer<'de>,
27+
{
28+
#[derive(Deserialize)]
29+
#[serde(rename_all = "kebab-case")]
30+
struct RawChecksumsMapping {
31+
sha256_hex: String,
32+
}
33+
34+
let RawChecksumsMapping { sha256_hex } = RawChecksumsMapping::deserialize(deserializer)?;
35+
sha256_hex.parse().map_err(D::Error::custom)
36+
}
37+
38+
impl FromStr for Sha256Sum {
39+
type Err = Error;
40+
41+
fn from_str(s: &str) -> Result<Self, Self::Err> {
42+
if s.len() != SHA256_SUM_HEX_LENGTH {
43+
anyhow::bail!(
44+
"cannot parse SHA-256: expected a {SHA256_SUM_HEX_LENGTH}-character long string"
45+
);
46+
}
47+
48+
let mut bytes = [0u8; 32];
49+
50+
bytes
51+
.iter_mut()
52+
.zip(s.as_bytes().chunks(2))
53+
.map(|(byte, hex_byte)| {
54+
let hex_str = str::from_utf8(hex_byte)?;
55+
*byte = u8::from_str_radix(hex_str, 16)?;
56+
Ok::<_, Self::Err>(())
57+
})
58+
.map(|result| result.context("cannot parse SHA-256: not a valid hex string"))
59+
.collect::<Result<Vec<()>, _>>()?;
60+
61+
Ok(Sha256Sum(bytes))
62+
}
63+
}
64+
65+
impl Deref for Sha256Sum {
66+
type Target = [u8; 32];
67+
68+
fn deref(&self) -> &Self::Target {
69+
&self.0
70+
}
71+
}
72+
73+
impl<Rhs> PartialEq<Rhs> for Sha256Sum
74+
where
75+
Rhs: Deref<Target = [u8]>,
76+
{
77+
fn eq(&self, other: &Rhs) -> bool {
78+
self.0 == **other
79+
}
80+
}

src/bashsupport.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
_SENTRY_TRACEBACK_FILE="___SENTRY_TRACEBACK_FILE___"
2-
_SENTRY_LOG_FILE="___SENTRY_LOG_FILE___"
1+
_SENTRY_TRACEBACK_FILE=___SENTRY_TRACEBACK_FILE___
2+
_SENTRY_LOG_FILE=___SENTRY_LOG_FILE___
33

44
if [ "${SENTRY_CLI_NO_EXIT_TRAP-0}" != 1 ]; then
55
trap _sentry_exit_trap EXIT
@@ -32,7 +32,7 @@ _sentry_err_trap() {
3232
echo "@exit_code:${_exit_code}" >> "$_SENTRY_TRACEBACK_FILE"
3333

3434
: >> "$_SENTRY_LOG_FILE"
35-
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___)
35+
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")
3636
rm -f "$_SENTRY_TRACEBACK_FILE" "$_SENTRY_LOG_FILE"
3737
}
3838

src/commands/bash_hook.rs

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ use anyhow::{format_err, Result};
1111
use clap::{builder::ArgPredicate, Arg, ArgAction, ArgMatches, Command};
1212
use lazy_static::lazy_static;
1313
use regex::Regex;
14-
use sentry::protocol::{Event, Exception, Frame, Stacktrace, User, Value};
14+
use sentry::protocol::{Event, Exception, Frame, Stacktrace, User};
1515
use uuid::Uuid;
1616

1717
use crate::commands::send_event;
1818
use crate::config::Config;
19+
use crate::utils::args::allow_xcode_infoplist_preprocessing_arg;
1920
use crate::utils::event::{attach_logfile, get_sdk_info};
2021
use crate::utils::releases::detect_release_name;
2122

@@ -40,15 +41,17 @@ pub fn make_command(command: Command) -> Command {
4041
.arg(
4142
Arg::new("no_environ")
4243
.long("no-environ")
44+
.hide(true)
4345
.action(ArgAction::SetTrue)
44-
.help("Do not send environment variables along"),
46+
.help("No-op, as we never send envrionment variables."),
4547
)
4648
.arg(
4749
Arg::new("cli")
4850
.long("cli")
4951
.value_name("CMD")
5052
.help("Explicitly set/override the sentry-cli command"),
5153
)
54+
.arg(allow_xcode_infoplist_preprocessing_arg())
5255
.arg(
5356
Arg::new("send_event")
5457
.long("send-event")
@@ -87,13 +90,15 @@ fn send_event(
8790
logfile: &str,
8891
tags: &[&String],
8992
release: Option<String>,
90-
environ: bool,
93+
allow_xcode_infoplist_preprocessing: bool,
9194
) -> Result<()> {
9295
let config = Config::current();
9396

9497
let mut event = Event {
9598
environment: config.get_environment().map(Into::into),
96-
release: release.or(detect_release_name().ok()).map(Into::into),
99+
release: release
100+
.or(detect_release_name(allow_xcode_infoplist_preprocessing).ok())
101+
.map(Into::into),
97102
sdk: Some(get_sdk_info()),
98103
user: whoami::fallible::username().ok().map(|n| User {
99104
username: Some(n),
@@ -112,13 +117,6 @@ fn send_event(
112117
event.tags.insert(key.into(), value.into());
113118
}
114119

115-
if environ {
116-
event.extra.insert(
117-
"environ".into(),
118-
Value::Object(env::vars().map(|(k, v)| (k, Value::String(v))).collect()),
119-
);
120-
}
121-
122120
let mut cmd = "unknown".to_owned();
123121
let mut exit_code = 1;
124122
let mut frames = vec![];
@@ -208,6 +206,10 @@ fn send_event(
208206
Ok(())
209207
}
210208

209+
fn shell_quote(value: &str) -> String {
210+
format!("'{}'", value.replace('\'', r"'\''"))
211+
}
212+
211213
pub fn execute(matches: &ArgMatches) -> Result<()> {
212214
let release = Config::current().get_release(matches).ok();
213215

@@ -222,7 +224,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
222224
matches.get_one::<String>("log").unwrap(),
223225
&tags,
224226
release,
225-
!matches.get_flag("no_environ"),
227+
matches.get_flag("allow_xcode_infoplist_preprocessing"),
226228
);
227229
}
228230

@@ -235,42 +237,45 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
235237
let mut script = BASH_SCRIPT
236238
.replace(
237239
"___SENTRY_TRACEBACK_FILE___",
238-
&traceback.display().to_string(),
240+
&shell_quote(&traceback.display().to_string()),
239241
)
240-
.replace("___SENTRY_LOG_FILE___", &log.display().to_string());
242+
.replace(
243+
"___SENTRY_LOG_FILE___",
244+
&shell_quote(&log.display().to_string()),
245+
);
241246

242247
script = script.replace(
243248
" ___SENTRY_TAGS___",
244249
&tags
245250
.iter()
246-
.map(|tag| format!(" --tag \"{tag}\""))
251+
.map(|tag| format!(" --tag {}", shell_quote(tag)))
247252
.collect::<Vec<_>>()
248253
.join(""),
249254
);
250255

251256
script = match release {
252257
Some(release) => script.replace(
253258
" ___SENTRY_RELEASE___",
254-
format!(" --release \"{release}\"").as_str(),
259+
format!(" --release {}", shell_quote(&release)).as_str(),
255260
),
256261
None => script.replace(" ___SENTRY_RELEASE___", ""),
257262
};
258263

259264
script = script.replace(
260265
"___SENTRY_CLI___",
261-
matches
262-
.get_one::<String>("cli")
263-
.map_or_else(
264-
|| env::current_exe().unwrap().display().to_string(),
265-
String::clone,
266-
)
267-
.as_str(),
266+
&shell_quote(&matches.get_one::<String>("cli").map_or_else(
267+
|| env::current_exe().unwrap().display().to_string(),
268+
String::clone,
269+
)),
268270
);
269271

270-
if matches.get_flag("no_environ") {
271-
script = script.replace("___SENTRY_NO_ENVIRON___", "--no-environ");
272+
if matches.get_flag("allow_xcode_infoplist_preprocessing") {
273+
script = script.replace(
274+
" ___SENTRY_ALLOW_XCODE_INFOPLIST_PREPROCESSING___",
275+
" --allow-xcode-infoplist-preprocessing",
276+
);
272277
} else {
273-
script = script.replace("___SENTRY_NO_ENVIRON___", "");
278+
script = script.replace(" ___SENTRY_ALLOW_XCODE_INFOPLIST_PREPROCESSING___", "");
274279
}
275280

276281
if !matches.get_flag("no_exit") {
@@ -279,3 +284,16 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
279284
println!("{script}");
280285
Ok(())
281286
}
287+
288+
#[cfg(test)]
289+
mod tests {
290+
use super::shell_quote;
291+
292+
#[test]
293+
fn shell_quote_handles_special_characters() {
294+
assert_eq!(
295+
shell_quote("it's $(unsafe); && ok"),
296+
"'it'\\''s $(unsafe); && ok'"
297+
);
298+
}
299+
}

src/commands/react_native/xcode.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ use serde_json::Value;
1818
use crate::api::Api;
1919
use crate::config::Config;
2020
use crate::constants::DEFAULT_MAX_WAIT;
21-
use crate::utils::args::{validate_distribution, ArgExt as _};
21+
use crate::utils::args::{
22+
allow_xcode_infoplist_preprocessing_arg, validate_distribution, ArgExt as _,
23+
};
2224
use crate::utils::file_search::ReleaseFileSearch;
2325
use crate::utils::file_upload::UploadContext;
2426
use crate::utils::fs::TempFile;
@@ -122,6 +124,7 @@ pub fn make_command(command: Command) -> Command {
122124
but at most for the given number of seconds.",
123125
),
124126
)
127+
.arg(allow_xcode_infoplist_preprocessing_arg())
125128
.arg(
126129
Arg::new("no_auto_release")
127130
.long("no-auto-release")
@@ -358,7 +361,9 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
358361
(Err(_), Err(_)) => {
359362
// Neither environment variable is present, attempt to parse Info.plist
360363
info!("Parsing Info.plist");
361-
match InfoPlist::discover_from_env() {
364+
match InfoPlist::discover_from_env(
365+
matches.get_flag("allow_xcode_infoplist_preprocessing"),
366+
) {
362367
Ok(Some(plist)) => {
363368
// Successfully discovered and parsed Info.plist
364369
let dist_string = plist.build().to_owned();
Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
use anyhow::Result;
22
use clap::{ArgMatches, Command};
33

4+
use crate::utils::args::allow_xcode_infoplist_preprocessing_arg;
45
use crate::utils::releases::detect_release_name;
56

67
pub fn make_command(command: Command) -> Command {
7-
command.about("Propose a version name for a new release.")
8+
command
9+
.about("Propose a version name for a new release.")
10+
.arg(allow_xcode_infoplist_preprocessing_arg())
811
}
912

10-
pub fn execute(_matches: &ArgMatches) -> Result<()> {
11-
println!("{}", detect_release_name()?);
13+
pub fn execute(matches: &ArgMatches) -> Result<()> {
14+
println!(
15+
"{}",
16+
detect_release_name(matches.get_flag("allow_xcode_infoplist_preprocessing"))?
17+
);
1218
Ok(())
1319
}

0 commit comments

Comments
 (0)