Skip to content

Commit c53bbd2

Browse files
committed
Merge branch 'fix-exploit'
2 parents 54a8495 + 114e91c commit c53bbd2

File tree

12 files changed

+146
-16
lines changed

12 files changed

+146
-16
lines changed

gix-transport/src/client/blocking_io/file.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,11 @@ impl client::Transport for SpawnProcessOnDemand {
211211
};
212212
cmd.stdin = Stdio::piped();
213213
cmd.stdout = Stdio::piped();
214+
if self.path.first() == Some(&b'-') {
215+
return Err(client::Error::AmbiguousPath {
216+
path: self.path.clone(),
217+
});
218+
}
214219
let repo_path = if self.ssh_cmd.is_some() {
215220
cmd.args.push(service.as_str().into());
216221
gix_quote::single(self.path.as_ref()).to_os_str_lossy().into_owned()
@@ -225,6 +230,7 @@ impl client::Transport for SpawnProcessOnDemand {
225230
}
226231
cmd.envs(std::mem::take(&mut self.envs));
227232

233+
gix_features::trace::debug!(command = ?cmd, "gix_transport::SpawnProcessOnDemand");
228234
let mut child = cmd.spawn().map_err(|err| client::Error::InvokeProgram {
229235
source: err,
230236
command: cmd_name.into_owned(),

gix-transport/src/client/blocking_io/ssh/mod.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use crate::{client::blocking_io, Protocol};
88
pub enum Error {
99
#[error("The scheme in \"{}\" is not usable for an ssh connection", .0.to_bstring())]
1010
UnsupportedScheme(gix_url::Url),
11+
#[error("Host name '{host}' could be mistaken for a command-line argument")]
12+
AmbiguousHostName { host: String },
1113
}
1214

1315
impl crate::IsSpuriousError for Error {}
@@ -37,12 +39,17 @@ pub mod invocation {
3739

3840
/// The error returned when producing ssh invocation arguments based on a selected invocation kind.
3941
#[derive(Debug, thiserror::Error)]
40-
#[error("The 'Simple' ssh variant doesn't support {function}")]
41-
pub struct Error {
42-
/// The simple command that should have been invoked.
43-
pub command: OsString,
44-
/// The function that was unsupported
45-
pub function: &'static str,
42+
#[allow(missing_docs)]
43+
pub enum Error {
44+
#[error("Host name '{host}' could be mistaken for a command-line argument")]
45+
AmbiguousHostName { host: String },
46+
#[error("The 'Simple' ssh variant doesn't support {function}")]
47+
Unsupported {
48+
/// The simple command that should have been invoked.
49+
command: OsString,
50+
/// The function that was unsupported
51+
function: &'static str,
52+
},
4653
}
4754
}
4855

@@ -105,7 +112,9 @@ pub fn connect(
105112
.stdin(Stdio::null())
106113
.with_shell()
107114
.arg("-G")
108-
.arg(url.host().expect("always set for ssh urls")),
115+
.arg(url.host_argument_safe().ok_or_else(|| Error::AmbiguousHostName {
116+
host: url.host().expect("set in ssh urls").into(),
117+
})?),
109118
)
110119
.status()
111120
.ok()

gix-transport/src/client/blocking_io/ssh/program_kind.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ impl ProgramKind {
3131
if disallow_shell {
3232
prepare.use_shell = false;
3333
}
34-
let host = url.host().expect("present in ssh urls");
3534
match self {
3635
ProgramKind::Ssh => {
3736
if desired_version != Protocol::V1 {
@@ -54,16 +53,26 @@ impl ProgramKind {
5453
}
5554
ProgramKind::Simple => {
5655
if url.port.is_some() {
57-
return Err(ssh::invocation::Error {
56+
return Err(ssh::invocation::Error::Unsupported {
5857
command: ssh_cmd.into(),
5958
function: "setting the port",
6059
});
6160
}
6261
}
6362
};
6463
let host_as_ssh_arg = match url.user() {
65-
Some(user) => format!("{user}@{host}"),
66-
None => host.into(),
64+
Some(user) => {
65+
let host = url.host().expect("present in ssh urls");
66+
format!("{user}@{host}")
67+
}
68+
None => {
69+
let host = url
70+
.host_argument_safe()
71+
.ok_or_else(|| ssh::invocation::Error::AmbiguousHostName {
72+
host: url.host().expect("ssh host always set").into(),
73+
})?;
74+
host.into()
75+
}
6776
};
6877

6978
// Try to force ssh to yield english messages (for parsing later)

gix-transport/src/client/blocking_io/ssh/tests.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,28 @@ mod program_kind {
144144
assert!(call_args(kind, "ssh://user@host:43/p", Protocol::V2).ends_with("-P 43 user@host"));
145145
}
146146
}
147+
#[test]
148+
fn ambiguous_host_is_allowed_with_user() {
149+
assert_eq!(
150+
call_args(ProgramKind::Ssh, "ssh://user@-arg/p", Protocol::V2),
151+
joined(&["ssh", "-o", "SendEnv=GIT_PROTOCOL", "user@-arg"])
152+
);
153+
}
154+
155+
#[test]
156+
fn ambiguous_host_is_disallowed() {
157+
assert!(matches!(
158+
try_call(ProgramKind::Ssh, "ssh://-arg/p", Protocol::V2),
159+
Err(ssh::invocation::Error::AmbiguousHostName { host }) if host == "-arg"
160+
));
161+
}
147162

148163
#[test]
149164
fn simple_cannot_handle_any_arguments() {
150-
match try_call(ProgramKind::Simple, "ssh://user@host:42/p", Protocol::V2) {
151-
Err(ssh::invocation::Error { .. }) => {}
152-
_ => panic!("BUG: unexpected outcome"),
153-
}
165+
assert!(matches!(
166+
try_call(ProgramKind::Simple, "ssh://user@host:42/p", Protocol::V2),
167+
Err(ssh::invocation::Error::Unsupported { .. })
168+
));
154169
assert_eq!(
155170
call_args(ProgramKind::Simple, "ssh://user@host/p", Protocol::V2),
156171
joined(&["simple", "user@host"]),

gix-transport/src/client/git/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,21 @@ mod message {
165165
"git-upload-pack hello\\world\0host=host:404\0"
166166
)
167167
}
168+
169+
#[test]
170+
fn with_strange_host_and_port() {
171+
assert_eq!(
172+
git::message::connect(
173+
Service::UploadPack,
174+
Protocol::V1,
175+
b"--upload-pack=attack",
176+
Some(&("--proxy=other-attack".into(), Some(404))),
177+
&[]
178+
),
179+
"git-upload-pack --upload-pack=attack\0host=--proxy=other-attack:404\0",
180+
"we explicitly allow possible `-arg` arguments to be passed to the git daemon - the remote must protect against exploitation, we don't want to prevent legitimate cases"
181+
)
182+
}
168183
}
169184
}
170185

gix-transport/src/client/non_io_types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ mod error {
138138
Http(#[from] HttpError),
139139
#[error(transparent)]
140140
SshInvocation(SshInvocationError),
141+
#[error("The repository path '{path}' could be mistaken for a command-line argument")]
142+
AmbiguousPath { path: BString },
141143
}
142144

143145
impl crate::IsSpuriousError for Error {

gix-transport/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ pub use gix_packetline as packetline;
2121
/// The version of the way client and server communicate.
2222
#[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
2323
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24-
#[allow(missing_docs)]
2524
pub enum Protocol {
2625
/// Version 0 is like V1, but doesn't show capabilities at all, at least when hosted without `git-daemon`.
2726
V0 = 0,

gix-url/src/lib.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ pub struct Url {
4848
/// The port to use when connecting to a host. If `None`, standard ports depending on `scheme` will be used.
4949
pub port: Option<u16>,
5050
/// The path portion of the URL, usually the location of the git repository.
51+
///
52+
/// # Security-Warning
53+
///
54+
/// URLs allow paths to start with `-` which makes it possible to mask command-line arguments as path which then leads to
55+
/// the invocation of programs from an attacker controlled URL. See <https://secure.phabricator.com/T12961> for details.
56+
///
57+
/// If this value is going to be used in a command-line application, call [Self::path_argument_safe()] instead.
5158
pub path: bstr::BString,
5259
}
5360

@@ -123,9 +130,34 @@ impl Url {
123130
self.password.as_deref()
124131
}
125132
/// Returns the host mentioned in the url, if present.
133+
///
134+
/// # Security-Warning
135+
///
136+
/// URLs allow hosts to start with `-` which makes it possible to mask command-line arguments as host which then leads to
137+
/// the invocation of programs from an attacker controlled URL. See <https://secure.phabricator.com/T12961> for details.
138+
///
139+
/// If this value is going to be used in a command-line application, call [Self::host_argument_safe()] instead.
126140
pub fn host(&self) -> Option<&str> {
127141
self.host.as_deref()
128142
}
143+
144+
/// Return the host of this URL if present *and* if it can't be mistaken for a command-line argument.
145+
///
146+
/// Use this method if the host is going to be passed to a command-line application.
147+
pub fn host_argument_safe(&self) -> Option<&str> {
148+
self.host().filter(|host| !looks_like_argument(host.as_bytes()))
149+
}
150+
151+
/// Return the path of this URL *and* if it can't be mistaken for a command-line argument.
152+
/// Note that it always begins with a slash, which is ignored for this comparison.
153+
///
154+
/// Use this method if the path is going to be passed to a command-line application.
155+
pub fn path_argument_safe(&self) -> Option<&BStr> {
156+
self.path
157+
.get(1..)
158+
.and_then(|truncated| (!looks_like_argument(truncated)).then_some(self.path.as_ref()))
159+
}
160+
129161
/// Returns true if the path portion of the url is `/`.
130162
pub fn path_is_root(&self) -> bool {
131163
self.path == "/"
@@ -146,6 +178,10 @@ impl Url {
146178
}
147179
}
148180

181+
fn looks_like_argument(b: &[u8]) -> bool {
182+
b.first() == Some(&b'-')
183+
}
184+
149185
/// Transformation
150186
impl Url {
151187
/// Turn a file url like `file://relative` into `file:///root/relative`, hence it assures the url's path component is absolute, using

gix-url/tests/access/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,23 @@ mod canonicalized {
2929
Ok(())
3030
}
3131
}
32+
33+
#[test]
34+
fn host_argument_safe() -> crate::Result {
35+
let url = gix_url::parse("ssh://-oProxyCommand=open$IFS-aCalculator/foo".into())?;
36+
assert_eq!(url.host(), Some("-oProxyCommand=open$IFS-aCalculator"));
37+
assert_eq!(url.host_argument_safe(), None);
38+
assert_eq!(url.path, "/foo");
39+
assert_eq!(url.path_argument_safe(), Some("/foo".into()));
40+
Ok(())
41+
}
42+
43+
#[test]
44+
fn path_argument_safe() -> crate::Result {
45+
let url = gix_url::parse("ssh://foo/-oProxyCommand=open$IFS-aCalculator".into())?;
46+
assert_eq!(url.host(), Some("foo"));
47+
assert_eq!(url.host_argument_safe(), Some("foo"));
48+
assert_eq!(url.path, "/-oProxyCommand=open$IFS-aCalculator");
49+
assert_eq!(url.path_argument_safe(), None);
50+
Ok(())
51+
}

tests/journey/gix.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,12 @@ title "gix commit-graph"
345345
}
346346
)
347347
)
348+
(with "an ambiguous ssh host which could be mistaken for an argument"
349+
it "fails without trying to pass it to command-line programs" && {
350+
WITH_SNAPSHOT="$snapshot/fail-ambigous-host" \
351+
expect_run $WITH_FAILURE "$exe_plumbing" free pack receive 'ssh://-oProxyCommand=open$IFS-aCalculator/foo'
352+
}
353+
)
348354
fi
349355
)
350356
elif [[ "$kind" = "small" ]]; then
@@ -355,6 +361,17 @@ title "gix commit-graph"
355361
fi
356362
)
357363
)
364+
if test "$kind" = "max" || test "$kind" = "max-pure"; then
365+
(with "the 'clone' sub-command"
366+
snapshot="$snapshot/clone"
367+
(with "an ambiguous ssh host which could be mistaken for an argument"
368+
it "fails without trying to pass it to command-line programs" && {
369+
WITH_SNAPSHOT="$snapshot/fail-ambigous-host" \
370+
expect_run $WITH_FAILURE "$exe_plumbing" clone 'ssh://-oProxyCommand=open$IFS-aCalculator/foo'
371+
}
372+
)
373+
)
374+
fi
358375
(with "the 'index' sub-command"
359376
snapshot="$snapshot/index"
360377
title "gix free pack index create"

tests/snapshots/plumbing/no-repo/pack/clone/fail-ambigous-host

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Error: Host name '-oProxyCommand=open$IFS-aCalculator' could be mistaken for a command-line argument
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Error: Host name '-oProxyCommand=open$IFS-aCalculator' could be mistaken for a command-line argument

0 commit comments

Comments
 (0)