diff --git a/crates/mind-body/src/self_run.rs b/crates/mind-body/src/self_run.rs index 0be6b66..754b93c 100644 --- a/crates/mind-body/src/self_run.rs +++ b/crates/mind-body/src/self_run.rs @@ -937,7 +937,7 @@ fn result_text(event: &serde_json::Map) -> String { /// RFC 3986 read it: it ends at the first `/`, `?`, `#`, backslash, quote, backtick, parenthesis, bracket, angle bracket or ASCII whitespace /// (`is_authority_end`). Its userinfo is everything before the authority's LAST `@` (a raw `@` in a password still redacts), and non-ASCII /// characters, NBSP and U+2028 included, stay inside it. The userinfo becomes ``: `scheme://@host`. With a colon it is -/// always redacted; without one it is a bare token on http, https, ftp, ftps, protocol-relative `//` and an unparseable scheme, and a plain +/// always redacted; without one it is a bare token on http, https, ftp, ftps, ws, wss, any `+http`/`+https` scheme (`git+https`), protocol-relative `//` and an unparseable scheme, and a plain /// username on any other scheme (`ssh://git@host`), which stays. A `@` after the authority (`github.com/@user`, `pkg@1.2.3`) is path, not /// userinfo, and is left alone. Scheme-less credentials are covered only in the shape `word:secret@host.tld/` at the start of an /// ASCII-whitespace-delimited word. Not covered: a password holding `/ ? # ` a quote or ASCII whitespace (no parser reads it as userinfo), @@ -963,7 +963,7 @@ pub(crate) fn redact_url_userinfo(text: &str) -> String { let end = body.find(is_authority_end).unwrap_or(body.len()); let authority = &body[..end]; match authority.rfind('@') { - Some(i) if i > 0 && (authority[..i].contains(':') || matches!(scheme.as_str(), "" | "http" | "https" | "ftp" | "ftps")) => { + Some(i) if i > 0 && (authority[..i].contains(':') || carries_bare_token(&scheme)) => { out.push_str(""); out.push_str(&authority[i..]); } @@ -975,6 +975,11 @@ pub(crate) fn redact_url_userinfo(text: &str) -> String { redact_schemeless(&out) } +/// Schemes whose lone userinfo is a token, not a username: web and ftp schemes, `ws`/`wss`, and `x+http`/`x+https` wrappers; `scheme` is lowercase. +fn carries_bare_token(scheme: &str) -> bool { + matches!(scheme, "" | "http" | "https" | "ftp" | "ftps" | "ws" | "wss") || scheme.ends_with("+http") || scheme.ends_with("+https") +} + /// ASCII whitespace; NBSP, U+2028 and other Unicode whitespace are not, so a secret holding them is redacted whole. fn is_break(c: char) -> bool { matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0b' | '\x0c') diff --git a/crates/mind-body/src/self_run/tests/reply.rs b/crates/mind-body/src/self_run/tests/reply.rs index 94229a0..1e20e4d 100644 --- a/crates/mind-body/src/self_run/tests/reply.rs +++ b/crates/mind-body/src/self_run/tests/reply.rs @@ -188,6 +188,11 @@ fn a_secret_is_redacted_whole_by_the_authority_rule_and_commonplace_urls_are_lef assert_eq!(r("HTTPS://TOKEN@h"), "HTTPS://@h"); assert_eq!(r("[l](https://T@h/x) `https://u:T@h`"), "[l](https://@h/x) @h> `https://@h`"); assert_eq!(r("a //u:T@host/x b //T@host c \"//T@h\" (//T@h)"), "a //@host/x b //@host c \"//@h\" (//@h)"); + // Token-bearing schemes beyond http: +http/+https wrappers and ws/wss redact a bare token; git+ssh and ssh keep their username. + assert_eq!(r("git+https://T@github.com/a/b GIT+HTTPS://T@h wss://T@h/ ws://T@h/"), "git+https://@github.com/a/b GIT+HTTPS://@h wss://@h/ ws://@h/"); + for same in ["git+ssh://git@host/x", "ssh://git@host/x"] { + assert_eq!(r(same), same); + } // Scheme-less: only word:secret@host.tld/ . Each guard of the shape has its own case. assert_eq!(r("user:T@host.tld/path"), "@host.tld/path"); for same in ["a:b@c", "a:b@host/x", "a:b@ho_st.tld/x", "u$er:T@host.tld/x", "a:@host.tld/x", "mail me@x.org/ and :T@host.tld/x"] {