Skip to content

Commit b06a0dd

Browse files
committed
fix: prevent hosts or paths that look like arguments to be passed to invoked commands.
See https://secure.phabricator.com/T12961 for more details.
1 parent d80b5f6 commit b06a0dd

File tree

8 files changed

+74
-19
lines changed

8 files changed

+74
-19
lines changed

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

+6
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

+16-7
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

+13-4
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

+19-4
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

+15
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

+2
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

-1
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

+3-3
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ pub struct Url {
5252
/// # Security-Warning
5353
///
5454
/// 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.
55+
/// the invocation of programs from an attacker controlled URL. See <https://secure.phabricator.com/T12961> for details.
5656
///
5757
/// If this value is going to be used in a command-line application, call [Self::path_argument_safe()] instead.
5858
pub path: bstr::BString,
@@ -134,7 +134,7 @@ impl Url {
134134
/// # Security-Warning
135135
///
136136
/// 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.
137+
/// the invocation of programs from an attacker controlled URL. See <https://secure.phabricator.com/T12961> for details.
138138
///
139139
/// If this value is going to be used in a command-line application, call [Self::host_argument_safe()] instead.
140140
pub fn host(&self) -> Option<&str> {
@@ -179,7 +179,7 @@ impl Url {
179179
}
180180

181181
fn looks_like_argument(b: &[u8]) -> bool {
182-
b.get(0) == Some(&b'-')
182+
b.first() == Some(&b'-')
183183
}
184184

185185
/// Transformation

0 commit comments

Comments
 (0)