redact: only ASCII whitespace ends a candidate run
NBSP, U+2028 and other Unicode whitespace stay inside userinfo, so a secret holding them is redacted whole. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2d9e959c56
commit
b19e57a6d3
2 changed files with 12 additions and 6 deletions
|
|
@ -934,7 +934,7 @@ fn result_text(event: &serde_json::Map<String, Value>) -> String {
|
|||
/// (`persona::structured_output`, string by string). It does not cover claude's stderr (inherited) or the CLI's transcript files.
|
||||
///
|
||||
/// A forge token rides in a remote URL, so every `scheme://`, and every protocol-relative `//`, opens a candidate: the run up to the next
|
||||
/// whitespace of any kind. Its userinfo is everything before the run's LAST `@`, whatever delimiters sit inside (fail closed: a path holding
|
||||
/// ASCII whitespace. Its userinfo is everything before the run's LAST `@`, whatever delimiters sit inside (fail closed: a path holding
|
||||
/// `@` is redacted too). The userinfo becomes `<redacted>`: `scheme://<redacted>@host`. With a colon (user:password) it is always
|
||||
/// redacted; without one it is a bare token on http, https, ftp, ftps and protocol-relative `//`, and a plain username on any other scheme
|
||||
/// (`ssh://git@host`), which stays. Scheme-less credentials are covered only in the shape `word:secret@host.tld/` at the start of a
|
||||
|
|
@ -945,7 +945,7 @@ pub(crate) fn redact_url_userinfo(text: &str) -> String {
|
|||
while let Some(at) = rest.find("//") {
|
||||
let (before, after) = rest.split_at(at);
|
||||
let body = &after[2..];
|
||||
let end = body.find(char::is_whitespace).unwrap_or(body.len());
|
||||
let end = body.find(is_break).unwrap_or(body.len());
|
||||
let run = &body[..end];
|
||||
out.push_str(before);
|
||||
out.push_str("//");
|
||||
|
|
@ -975,6 +975,11 @@ pub(crate) fn redact_url_userinfo(text: &str) -> String {
|
|||
redact_schemeless(&out)
|
||||
}
|
||||
|
||||
/// ASCII whitespace ends a candidate; NBSP, U+2028 and other Unicode whitespace do not, so a secret holding them is redacted whole.
|
||||
fn is_break(c: char) -> bool {
|
||||
matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0b' | '\x0c')
|
||||
}
|
||||
|
||||
/// `word:secret@host.tld/` at the start of a whitespace-delimited word that holds no `//` becomes `<redacted>@host.tld/`.
|
||||
fn redact_schemeless(text: &str) -> String {
|
||||
let mut out = String::with_capacity(text.len());
|
||||
|
|
@ -984,7 +989,7 @@ fn redact_schemeless(text: &str) -> String {
|
|||
word.clear();
|
||||
};
|
||||
for c in text.chars() {
|
||||
if c.is_whitespace() {
|
||||
if is_break(c) {
|
||||
flush(&mut word, &mut out);
|
||||
out.push(c);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -160,13 +160,14 @@ fn a_credential_split_by_the_reply_limit_is_redacted_before_the_cut() {
|
|||
#[test]
|
||||
fn delimiters_inside_a_secret_leak_none_of_it_and_the_bare_and_scheme_less_forms_are_covered() {
|
||||
let r = redact_url_userinfo;
|
||||
for secret in ["pa/ssT", "pa?ssT", "pa#ssT", "pa\"ssT", "pa'ssT", "pa)ssT", "pa<ssT", "pa]ssT", "[u]"] {
|
||||
for secret in ["pa/ssT", "pa?ssT", "pa#ssT", "pa\"ssT", "pa'ssT", "pa)ssT", "pa\u{a0}ssT", "pa\u{2028}ssT", "pa<ssT", "pa]ssT", "[u]"] {
|
||||
let out = r(&format!("x https://u:{secret}@h/x"));
|
||||
assert!(out.starts_with("x https://<redacted>@h"), "{secret:?} -> {out:?}");
|
||||
assert!(!out.contains("ssT"), "{secret:?} -> {out:?}");
|
||||
}
|
||||
// Whitespace of any kind ends a candidate run, so a secret holding it is cut there.
|
||||
assert_eq!(r("https://u:pa\u{a0}ssT@h/x"), "https://u:pa\u{a0}ssT@h/x");
|
||||
// Only ASCII whitespace ends a candidate run.
|
||||
assert_eq!(r("https://u:pa ssT@h/x"), "https://u:pa ssT@h/x");
|
||||
assert_eq!(r("https://u:pa\ttoken@h/x"), "https://u:pa\ttoken@h/x");
|
||||
assert_eq!(r("HTTPS://TOKEN@h"), "HTTPS://<redacted>@h");
|
||||
assert_eq!(r("[l](https://T@h/x) <https://T@h> `https://u:T@h`"), "[l](https://<redacted>@h/x) <https://<redacted>@h> `https://<redacted>@h`");
|
||||
assert_eq!(r("a //u:T@host/x b //T@host c"), "a //<redacted>@host/x b //<redacted>@host c");
|
||||
|
|
|
|||
Loading…
Reference in a new issue