Skip to content

Commit 9e7f407

Browse files
fix(uri): omit endpoint values from errors
1 parent 0fa0585 commit 9e7f407

1 file changed

Lines changed: 31 additions & 108 deletions

File tree

‎src/sinks/util/uri.rs‎

Lines changed: 31 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -199,34 +199,27 @@ pub fn protocol_endpoint(uri: Uri) -> (String, String) {
199199
}
200200

201201
/// Error returned when a configured endpoint cannot be used as an absolute HTTP URL.
202+
///
203+
/// Endpoint and path values are deliberately omitted so configuration errors
204+
/// cannot retain or display embedded credentials or tokens.
202205
#[derive(Debug, Snafu)]
203206
pub enum HttpEndpointError {
204-
#[snafu(display("endpoint `{endpoint}` is not a valid URI: {source}"))]
205-
InvalidUri {
206-
endpoint: String,
207-
source: http::uri::InvalidUri,
208-
},
209-
210-
#[snafu(display("endpoint `{endpoint}` has an invalid path `{path}`: {source}"))]
211-
InvalidPath {
212-
endpoint: String,
213-
path: String,
214-
source: http::uri::InvalidUri,
215-
},
216-
217-
#[snafu(display("endpoint `{endpoint}` cannot be reassembled from its parts: {source}"))]
218-
InvalidUriParts {
219-
endpoint: String,
220-
source: http::uri::InvalidUriParts,
221-
},
207+
#[snafu(display("endpoint is not a valid URI: {source}"))]
208+
InvalidUri { source: http::uri::InvalidUri },
209+
210+
#[snafu(display("endpoint has an invalid path: {source}"))]
211+
InvalidPath { source: http::uri::InvalidUri },
212+
213+
#[snafu(display("endpoint cannot be reassembled from its parts: {source}"))]
214+
InvalidUriParts { source: http::uri::InvalidUriParts },
222215

223216
#[snafu(display(
224-
"endpoint must be an absolute http(s) URL, for example `https://example.com`; got `{endpoint}`"
217+
"endpoint must be an absolute http(s) URL, for example `https://example.com`"
225218
))]
226-
NotAbsoluteHttp { endpoint: String },
219+
NotAbsoluteHttp,
227220

228-
#[snafu(display("endpoint `{endpoint}` has an invalid port"))]
229-
InvalidPort { endpoint: String },
221+
#[snafu(display("endpoint has an invalid port"))]
222+
InvalidPort,
230223
}
231224

232225
/// A `Uri` proven to be an absolute `http`/`https` URL.
@@ -276,14 +269,10 @@ impl HttpEndpoint {
276269
let has_valid_scheme_and_host = matches!(uri.scheme_str(), Some("http" | "https"))
277270
&& uri.host().is_some_and(|host| !host.is_empty());
278271
if !has_valid_scheme_and_host {
279-
return Err(HttpEndpointError::NotAbsoluteHttp {
280-
endpoint: redact_userinfo(&uri.to_string()),
281-
});
272+
return Err(HttpEndpointError::NotAbsoluteHttp);
282273
}
283274
if authority_has_invalid_port(&uri) {
284-
return Err(HttpEndpointError::InvalidPort {
285-
endpoint: redact_userinfo(&uri.to_string()),
286-
});
275+
return Err(HttpEndpointError::InvalidPort);
287276
}
288277
Ok(Self(uri))
289278
}
@@ -300,14 +289,10 @@ impl HttpEndpoint {
300289
// the scheme is added up front rather than relying on the parser to
301290
// accept authority-form input.
302291
let uri = if has_scheme(endpoint) {
303-
endpoint.parse::<Uri>().context(InvalidUriSnafu {
304-
endpoint: redact_userinfo(endpoint),
305-
})?
292+
endpoint.parse::<Uri>().context(InvalidUriSnafu)?
306293
} else {
307294
let endpoint = format!("https://{endpoint}");
308-
endpoint.parse::<Uri>().context(InvalidUriSnafu {
309-
endpoint: redact_userinfo(&endpoint),
310-
})?
295+
endpoint.parse::<Uri>().context(InvalidUriSnafu)?
311296
};
312297
Self::new(uri)
313298
}
@@ -375,13 +360,8 @@ impl HttpEndpoint {
375360
} else {
376361
format!("{base_path}/{}", path.strip_prefix('/').unwrap_or(path))
377362
};
378-
parts.path_and_query = Some(joined.parse::<PathAndQuery>().context(InvalidPathSnafu {
379-
endpoint: redact_userinfo(&self.0.to_string()),
380-
path: joined,
381-
})?);
382-
let uri = Uri::from_parts(parts).context(InvalidUriPartsSnafu {
383-
endpoint: redact_userinfo(&self.0.to_string()),
384-
})?;
363+
parts.path_and_query = Some(joined.parse::<PathAndQuery>().context(InvalidPathSnafu)?);
364+
let uri = Uri::from_parts(parts).context(InvalidUriPartsSnafu)?;
385365
Self::new(uri)
386366
}
387367

@@ -401,13 +381,8 @@ impl HttpEndpoint {
401381
.map(PathAndQuery::path)
402382
.unwrap_or_default();
403383
let joined = format!("{base_path}{suffix}");
404-
parts.path_and_query = Some(joined.parse::<PathAndQuery>().context(InvalidPathSnafu {
405-
endpoint: redact_userinfo(&self.0.to_string()),
406-
path: joined,
407-
})?);
408-
let uri = Uri::from_parts(parts).context(InvalidUriPartsSnafu {
409-
endpoint: redact_userinfo(&self.0.to_string()),
410-
})?;
384+
parts.path_and_query = Some(joined.parse::<PathAndQuery>().context(InvalidPathSnafu)?);
385+
let uri = Uri::from_parts(parts).context(InvalidUriPartsSnafu)?;
411386
Self::new(uri)
412387
}
413388
}
@@ -464,59 +439,6 @@ fn has_scheme(endpoint: &str) -> bool {
464439
&& chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
465440
}
466441

467-
/// Redacts userinfo from an endpoint string for error messages, so embedded
468-
/// basic-auth credentials are not echoed into logs or command output.
469-
///
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.
474-
fn redact_userinfo(endpoint: &str) -> String {
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 {
505-
return endpoint.to_string();
506-
};
507-
// The authority ends at the first `/`, `?`, or `#`; `find` returns the
508-
// byte offset of an ASCII delimiter, which is a valid char boundary.
509-
let (authority, tail) = match rest.find(['/', '?', '#']) {
510-
Some(i) => rest.split_at(i),
511-
None => (rest, ""),
512-
};
513-
let Some((_, host_port)) = authority.rsplit_once('@') else {
514-
return endpoint.to_string();
515-
};
516-
let prefix = endpoint.get(..authority_start).unwrap_or_default();
517-
format!("{prefix}<redacted>@{host_port}{tail}")
518-
}
519-
520442
#[cfg(test)]
521443
mod tests {
522444
use super::*;
@@ -669,10 +591,10 @@ mod tests {
669591
assert!(
670592
matches!(
671593
HttpEndpoint::parse(endpoint),
672-
Err(HttpEndpointError::NotAbsoluteHttp { .. })
594+
Err(HttpEndpointError::NotAbsoluteHttp)
673595
| Err(HttpEndpointError::InvalidUri { .. })
674596
| Err(HttpEndpointError::InvalidUriParts { .. })
675-
| Err(HttpEndpointError::InvalidPort { .. })
597+
| Err(HttpEndpointError::InvalidPort)
676598
),
677599
"expected `{endpoint}` to be rejected"
678600
);
@@ -693,7 +615,7 @@ mod tests {
693615
] {
694616
assert!(matches!(
695617
HttpEndpoint::parse(endpoint),
696-
Err(HttpEndpointError::InvalidPort { .. })
618+
Err(HttpEndpointError::InvalidPort)
697619
));
698620
}
699621
}
@@ -704,16 +626,17 @@ mod tests {
704626
"http://user:secret@localhost:notaport",
705627
"user:secret@exa mple.com",
706628
"http:/user:secret@example.com",
629+
"http:://user:secret@example.com",
707630
] {
708631
let error = HttpEndpoint::parse(endpoint).expect_err("endpoint should be rejected");
709632
let message = error.to_string();
710633
assert!(
711-
!message.contains("secret"),
712-
"error must not echo the password: {message}"
634+
!message.contains(endpoint),
635+
"error must not echo the endpoint: {message}"
713636
);
714637
assert!(
715-
message.contains("<redacted>@"),
716-
"error should mark the redacted userinfo: {message}"
638+
!message.contains("secret"),
639+
"error must not echo the password: {message}"
717640
);
718641
}
719642
}

0 commit comments

Comments
 (0)