Skip to content

Commit 29941e6

Browse files
committed
Sketch *_as_argument methods and supporting enum
This isn't correct yet, because: - It doesn't compile because of the overcomplicated way I'm doing borrowing. The enum can just have references as type arguments instead. - The enum should be public. (Those are noted here rather than in the next commit that will fix them, because it is not useful to include them in a message generated from it if it is titled as a conventional commit.) In addition, while some existing methods are implemented in terms of these now so there is some test coverage that way, it's far from complete and these really need their own tests.
1 parent beef8d2 commit 29941e6

File tree

1 file changed

+84
-20
lines changed

1 file changed

+84
-20
lines changed

gix-url/src/lib.rs

Lines changed: 84 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,26 @@ pub fn expand_path(user: Option<&expand_path::ForUser>, path: &BStr) -> Result<P
5555
})
5656
}
5757

58+
/// Classification of a portion of a URL by whether it is *syntactically* safe to pass as an argument to a command-line program.
59+
///
60+
/// Various parts of URLs can be specified to begin with `-`. If they are used as options to a command-line application
61+
/// such as an SSH client, they will be treated as options rather than as non-option arguments as the developer intended.
62+
/// This is a security risk, because URLs are not always trusted and can often be composed or influenced by an attacker.
63+
/// See <https://secure.phabricator.com/T12961> for details.
64+
///
65+
/// # Security Warning
66+
///
67+
/// This type only expresses known *syntactic* risk. It does not cover other risks, such as passing a personal access
68+
/// token as a username rather than a password in an application that logs usernames.
69+
enum ArgumentSafety<'a, T> {
70+
/// May be safe. There is nothing to pass, so there is nothing dangerous.
71+
Absent,
72+
/// May be safe. The argument does not begin with a `-` and so will not be confused as an option.
73+
Usable(&'a T),
74+
/// Dangerous! Begins with `-` and could be treated as an option. Use the value in error messages only.
75+
Dangerous(&'a T),
76+
}
77+
5878
/// A URL with support for specialized git related capabilities.
5979
///
6080
/// Additionally there is support for [deserialization](Url::from_bytes()) and [serialization](Url::to_bstring()).
@@ -85,7 +105,7 @@ pub struct Url {
85105
pub port: Option<u16>,
86106
/// The path portion of the URL, usually the location of the git repository.
87107
///
88-
/// # Security-Warning
108+
/// # Security Warning
89109
///
90110
/// URLs allow paths to start with `-` which makes it possible to mask command-line arguments as path which then leads to
91111
/// the invocation of programs from an attacker controlled URL. See <https://secure.phabricator.com/T12961> for details.
@@ -164,9 +184,9 @@ impl Url {
164184

165185
/// Access
166186
impl Url {
167-
/// Return the user mentioned in the URL, if present.
187+
/// Return the username mentioned in the URL, if present.
168188
///
169-
/// # Security-Warning
189+
/// # Security Warning
170190
///
171191
/// URLs allow usernames to start with `-` which makes it possible to mask command-line arguments as username which then leads to
172192
/// the invocation of programs from an attacker controlled URL. See <https://secure.phabricator.com/T12961> for details.
@@ -176,19 +196,28 @@ impl Url {
176196
self.user.as_deref()
177197
}
178198

179-
/// Return the user from this URL if present *and* if it can't be mistaken for a command-line argument.
199+
/// Classify the username of this URL by whether it is safe to pass as a command-line argument.
180200
///
181-
/// Use this method if the user or a portion of the URL that begins with it will be passed to a command-line application.
201+
/// Use this method instead of [Self::user()] if the host is going to be passed to a command-line application.
202+
/// If the unsafe and absent cases need not be distinguished, [Self::user_argument_safe()] may also be used.
203+
pub fn user_as_argument(&self) -> ArgumentSafety<str> {
204+
match self.user() {
205+
Some(user) if looks_like_command_line_option(user.as_bytes()) => ArgumentSafety::Dangerous(user),
206+
Some(user) => ArgumentSafety::Usable(user),
207+
None => ArgumentSafety::Absent,
208+
}
209+
}
210+
211+
/// Return the username of this URL if present *and* if it can't be mistaken for a command-line argument.
212+
///
213+
/// Use this method or [Self::user_as_argument()] instead of [Self::user()] if the host is going to be
214+
/// passed to a command-line application. Prefer [Self::user_as_argument()] unless the unsafe and absent
215+
/// cases need not be distinguished from each other.
182216
pub fn user_argument_safe(&self) -> Option<&str> {
183-
// FIXME: A return value of None from this method, or host_argument_safe(), is ambiguous: the user (or host) is
184-
// either present but unsafe, or absent. Furthermore, in practice the value is usually needed even if unsafe,
185-
// in order to report it in an error message. In gix-transport, the ambiguity makes it easy to write a new bug
186-
// while using this interface for user_argument_safe(). In contrast, in host_argument_safe(), the ambiguity is
187-
// much less of a problem, because the host is expected to be present. Yet the host() method must still be
188-
// called when handling the None case, to include it in the error. If possible, both methods should be replaced
189-
// by methods with a richer return type (a new enum). If not, the ambiguity should be prominently documented.
190-
self.user()
191-
.filter(|user| !looks_like_command_line_option(user.as_bytes()))
217+
match self.user_as_argument() {
218+
ArgumentSafety::Usable(user) => Some(user),
219+
_ => None,
220+
}
192221
}
193222

194223
/// Return the password mentioned in the url, if present.
@@ -198,28 +227,63 @@ impl Url {
198227

199228
/// Return the host mentioned in the URL, if present.
200229
///
201-
/// # Security-Warning
230+
/// # Security Warning
202231
///
203232
/// URLs allow hosts to start with `-` which makes it possible to mask command-line arguments as host which then leads to
204233
/// the invocation of programs from an attacker controlled URL. See <https://secure.phabricator.com/T12961> for details.
205234
///
206-
/// If this value is ever going to be passed to a command-line application, call [Self::host_argument_safe()] instead.
235+
/// If this value is ever going to be passed to a command-line application, call [Self::host_as_argument()]
236+
/// or [Self::host_argument_safe()] instead.
207237
pub fn host(&self) -> Option<&str> {
208238
self.host.as_deref()
209239
}
210240

241+
/// Classify the host of this URL by whether it is safe to pass as a command-line argument.
242+
///
243+
/// Use this method instead of [Self::host()] if the host is going to be passed to a command-line application.
244+
/// If the unsafe and absent cases need not be distinguished, [Self::host_argument_safe()] may also be used.
245+
pub fn host_as_argument(&self) -> ArgumentSafety<str> {
246+
match self.host() {
247+
Some(host) if looks_like_command_line_option(host.as_bytes()) => ArgumentSafety::Dangerous(host),
248+
Some(host) => ArgumentSafety::Usable(host),
249+
None => ArgumentSafety::Absent,
250+
}
251+
}
252+
211253
/// Return the host of this URL if present *and* if it can't be mistaken for a command-line argument.
212254
///
213-
/// Use this method if the host is going to be passed to a command-line application.
255+
/// Use this method or [Self::host_as_argument()] instead of [Self::host()] if the host is going to be
256+
/// passed to a command-line application. Prefer [Self::host_as_argument()] unless the unsafe and absent
257+
/// cases need not be distinguished from each other.
214258
pub fn host_argument_safe(&self) -> Option<&str> {
215-
self.host()
216-
.filter(|host| !looks_like_command_line_option(host.as_bytes()))
259+
match self.host_as_argument() {
260+
ArgumentSafety::Usable(host) => Some(host),
261+
_ => None,
262+
}
263+
}
264+
265+
/// Classify the path of this URL by whether it is safe to pass as a command-line argument.
266+
/// Note that it always begins with a slash, which is ignored for this classification.
267+
///
268+
/// Use this method or [Self::path_argument_safe()] instead of [Self::path] if the host is going to be
269+
/// passed to a command-line application, unless it is certain that the leading `/` will always be included.
270+
///
271+
/// This method never returns an [ArgumentSafety::Absent].
272+
pub fn path_as_argument(&self) -> ArgumentSafety<BStr> {
273+
match self.path_argument_safe() {
274+
Some(path) => ArgumentSafety::Usable(path),
275+
None => ArgumentSafety::Dangerous(self.path.as_ref()),
276+
}
217277
}
218278

219279
/// Return the path of this URL *if* it can't be mistaken for a command-line argument.
220280
/// Note that it always begins with a slash, which is ignored for this comparison.
221281
///
222-
/// Use this method if the path is going to be passed to a command-line application.
282+
/// Use this method instead of accessing [Self::path] directly if the path is going to be passed to a
283+
/// command-line application, unless it is certain that the leading `/` will always be included.
284+
///
285+
/// The result of this method is unambiguous because [Self::path] is not an option type, but
286+
/// [Self::path_as_argument()] may also safely be used.
223287
pub fn path_argument_safe(&self) -> Option<&BStr> {
224288
self.path
225289
.get(1..)

0 commit comments

Comments
 (0)