Skip to content

Commit 530257f

Browse files
committed
fix: assure passwords aren't returned in percent-encoded form.
1 parent d281ba6 commit 530257f

File tree

7 files changed

+43
-10
lines changed

7 files changed

+43
-10
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-url/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ serde = { version = "1.0.114", optional = true, default-features = false, featur
2626
thiserror = "2.0.0"
2727
url = "2.5.2"
2828
bstr = { version = "1.3.0", default-features = false, features = ["std"] }
29+
percent-encoding = "2.3.1"
2930

3031
document-features = { version = "0.2.0", optional = true }
3132

gix-url/src/impls.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ impl std::fmt::Display for Url {
8181
let mut storage;
8282
let to_print = if self.password.is_some() {
8383
storage = self.clone();
84-
storage.password = Some("<redacted>".into());
84+
storage.password = Some("redacted".into());
8585
&storage
8686
} else {
8787
self

gix-url/src/lib.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,10 @@ impl Url {
308308
}
309309
}
310310

311+
fn percent_encode(s: &str) -> Cow<'_, str> {
312+
percent_encoding::utf8_percent_encode(s, percent_encoding::NON_ALPHANUMERIC).into()
313+
}
314+
311315
/// Serialization
312316
impl Url {
313317
/// Write this URL losslessly to `out`, ready to be parsed again.
@@ -318,10 +322,10 @@ impl Url {
318322
}
319323
match (&self.user, &self.host) {
320324
(Some(user), Some(host)) => {
321-
out.write_all(user.as_bytes())?;
325+
out.write_all(percent_encode(user).as_bytes())?;
322326
if let Some(password) = &self.password {
323327
out.write_all(b":")?;
324-
out.write_all(password.as_bytes())?;
328+
out.write_all(percent_encode(password).as_bytes())?;
325329
}
326330
out.write_all(b"@")?;
327331
out.write_all(host.as_bytes())?;

gix-url/src/parse.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use std::convert::Infallible;
22

3-
use bstr::{BStr, BString, ByteSlice};
4-
53
use crate::Scheme;
4+
use bstr::{BStr, BString, ByteSlice};
5+
use percent_encoding::percent_decode_str;
66

77
/// The error returned by [parse()](crate::parse()).
88
#[derive(Debug, thiserror::Error)]
@@ -115,13 +115,20 @@ pub(crate) fn url(input: &BStr, protocol_end: usize) -> Result<crate::Url, Error
115115
serialize_alternative_form: false,
116116
scheme,
117117
user: url_user(&url),
118-
password: url.password().map(Into::into),
118+
password: url.password().map(percent_decoded_utf8),
119119
host: url.host_str().map(Into::into),
120120
port: url.port(),
121121
path: url.path().into(),
122122
})
123123
}
124124

125+
fn percent_decoded_utf8(s: &str) -> String {
126+
percent_decode_str(s)
127+
.decode_utf8()
128+
.expect("it's not possible to sneak illegal UTF8 into a URL")
129+
.into_owned()
130+
}
131+
125132
pub(crate) fn scp(input: &BStr, colon: usize) -> Result<crate::Url, Error> {
126133
let input = input_to_utf8(input, UrlKind::Scp)?;
127134

@@ -151,7 +158,7 @@ pub(crate) fn scp(input: &BStr, colon: usize) -> Result<crate::Url, Error> {
151158
serialize_alternative_form: true,
152159
scheme: url.scheme().into(),
153160
user: url_user(&url),
154-
password: url.password().map(Into::into),
161+
password: url.password().map(percent_decoded_utf8),
155162
host: url.host_str().map(Into::into),
156163
port: url.port(),
157164
path: path.into(),
@@ -162,7 +169,7 @@ fn url_user(url: &url::Url) -> Option<String> {
162169
if url.username().is_empty() && url.password().is_none() {
163170
None
164171
} else {
165-
Some(url.username().into())
172+
Some(percent_decoded_utf8(url.username()))
166173
}
167174
}
168175

gix-url/tests/access/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ fn display() {
174174
compare("/path/to/repo", "/path/to/repo", "same goes for simple paths");
175175
compare(
176176
"https://user:password@host/path",
177-
"https://user:<redacted>@host/path",
178-
"it visibly redacts passwords though",
177+
"https://user:redacted@host/path",
178+
"it visibly redacts passwords though, and it's still a valid URL",
179179
);
180180
}

gix-url/tests/parse/http.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,26 @@ fn username_and_password_and_port() -> crate::Result {
3939
)
4040
}
4141

42+
#[test]
43+
fn username_and_password_with_spaces_and_port() -> crate::Result {
44+
let expected = gix_url::Url::from_parts(
45+
Scheme::Http,
46+
Some("user name".into()),
47+
Some("password secret".into()),
48+
Some("example.com".into()),
49+
Some(8080),
50+
b"/~byron/hello".into(),
51+
false,
52+
)?;
53+
assert_url_roundtrip(
54+
"http://user%20name:password%20secret@example.com:8080/~byron/hello",
55+
expected.clone(),
56+
)?;
57+
assert_eq!(expected.user(), Some("user name"));
58+
assert_eq!(expected.password(), Some("password secret"));
59+
Ok(())
60+
}
61+
4262
#[test]
4363
fn only_password() -> crate::Result {
4464
assert_url_roundtrip(

0 commit comments

Comments
 (0)