Skip to content

Commit 0fa0585

Browse files
fix(uri): redact malformed endpoint credentials
1 parent c524169 commit 0fa0585

1 file changed

Lines changed: 37 additions & 4 deletions

File tree

‎src/sinks/util/uri.rs‎

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -467,10 +467,41 @@ fn has_scheme(endpoint: &str) -> bool {
467467
/// Redacts userinfo from an endpoint string for error messages, so embedded
468468
/// basic-auth credentials are not echoed into logs or command output.
469469
///
470-
/// Only the authority portion (between the scheme and the first `/`, `?`, or
471-
/// `#`) is inspected; an `@` later in the path or query is left untouched.
470+
/// Leading scheme prefixes are skipped before inspecting the authority-like
471+
/// segment. Repeating this handles a defaulted `https://` prefix followed by a
472+
/// malformed original prefix such as `http:/`. An `@` later in the path or
473+
/// query is left untouched.
472474
fn redact_userinfo(endpoint: &str) -> String {
473-
let Some((scheme, rest)) = endpoint.split_once("://") else {
475+
let mut authority_start = 0;
476+
while let Some(rest) = endpoint.get(authority_start..) {
477+
let Some(scheme_end) = rest.find(':') else {
478+
break;
479+
};
480+
let Some(scheme) = rest.get(..scheme_end) else {
481+
break;
482+
};
483+
let mut chars = scheme.chars();
484+
let valid_scheme = matches!(chars.next(), Some(c) if c.is_ascii_alphabetic())
485+
&& chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'));
486+
if !valid_scheme {
487+
break;
488+
}
489+
490+
let after_colon = authority_start + scheme_end + 1;
491+
let slash_count = endpoint
492+
.get(after_colon..)
493+
.unwrap_or_default()
494+
.bytes()
495+
.take_while(|byte| *byte == b'/')
496+
.count();
497+
let is_http = scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https");
498+
if slash_count == 0 && !is_http {
499+
break;
500+
}
501+
authority_start = after_colon + slash_count;
502+
}
503+
504+
let Some(rest) = endpoint.get(authority_start..) else {
474505
return endpoint.to_string();
475506
};
476507
// The authority ends at the first `/`, `?`, or `#`; `find` returns the
@@ -482,7 +513,8 @@ fn redact_userinfo(endpoint: &str) -> String {
482513
let Some((_, host_port)) = authority.rsplit_once('@') else {
483514
return endpoint.to_string();
484515
};
485-
format!("{scheme}://<redacted>@{host_port}{tail}")
516+
let prefix = endpoint.get(..authority_start).unwrap_or_default();
517+
format!("{prefix}<redacted>@{host_port}{tail}")
486518
}
487519

488520
#[cfg(test)]
@@ -671,6 +703,7 @@ mod tests {
671703
for endpoint in [
672704
"http://user:secret@localhost:notaport",
673705
"user:secret@exa mple.com",
706+
"http:/user:secret@example.com",
674707
] {
675708
let error = HttpEndpoint::parse(endpoint).expect_err("endpoint should be rejected");
676709
let message = error.to_string();

0 commit comments

Comments
 (0)