Skip to content

Commit 5573405

Browse files
committed
fix(security): close Bearer/Basic tab/newline redaction bypass (PURPLE NO-SHIP)
- match scheme word + ANY whitespace at every occurrence; prior literal-space match missed Authorization newline Bearer tab token (no-code-exec leak) - regression test for tab/newline-after-scheme + multi-occurrence + Basic - gate fix: clippy allow on holistic_red_r2_poc so workspace clippy -D warnings passes; 2186 tests pass
1 parent 2502efa commit 5573405

1 file changed

Lines changed: 48 additions & 9 deletions

File tree

crates/clx-core/src/redaction.rs

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -247,20 +247,29 @@ pub fn redact_secrets(text: &str) -> String {
247247
// has the scheme prefix consumed first; otherwise section 2b would
248248
// eat just the `bearer` word and leave the token behind.
249249
// -------------------------------------------------------------------------
250-
for scheme in &["bearer ", "basic "] {
251-
// R1-A: scan ALL occurrences, not just the first - a blob with a second
252-
// `Authorization: Bearer <token>` previously leaked the later tokens.
250+
for scheme in &["bearer", "basic"] {
251+
// R1-A + PURPLE follow-up: match the scheme WORD then ANY whitespace
252+
// (space/tab/newline), at EVERY occurrence. The prior `"bearer "`
253+
// literal-space match missed `Bearer\t...` / `Bearer\n...` and is a
254+
// no-code-exec leak into logs/audit (Codex PURPLE NO-SHIP).
253255
let mut from = 0usize;
254256
loop {
255257
let lower_search = redacted.to_lowercase();
256-
let Some(rel) = lower_search[from..].find(scheme) else {
258+
let Some(rel) = lower_search.get(from..).and_then(|s| s.find(scheme)) else {
257259
break;
258260
};
259-
let scheme_start = from + rel;
260-
let token_start = scheme_start + scheme.len();
261-
if redacted.len() <= token_start + 6 {
262-
break;
261+
let after_kw = from + rel + scheme.len();
262+
// Require >=1 whitespace after the scheme word so `bearertoken`
263+
// is not treated as a scheme prefix.
264+
let mut cursor = after_kw;
265+
while cursor < redacted.len() && redacted.as_bytes()[cursor].is_ascii_whitespace() {
266+
cursor += 1;
267+
}
268+
if cursor == after_kw {
269+
from = after_kw;
270+
continue;
263271
}
272+
let token_start = cursor;
264273
let token_end = redacted[token_start..]
265274
.find(|c: char| c.is_whitespace() || c == '"' || c == '\'')
266275
.map_or(redacted.len(), |i| token_start + i);
@@ -270,7 +279,7 @@ pub fn redact_secrets(text: &str) -> String {
270279
redacted.replace_range(token_start..token_end, "***REDACTED***");
271280
from = token_start + "***REDACTED***".len();
272281
} else {
273-
from = token_end;
282+
from = after_kw;
274283
}
275284
}
276285
}
@@ -592,6 +601,36 @@ mod tests {
592601
assert!(!redacted.contains("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"));
593602
}
594603

604+
// Codex PURPLE NO-SHIP regression: scheme followed by tab/newline (not just
605+
// a literal space) must still redact the token, at EVERY occurrence.
606+
#[test]
607+
fn test_redact_secrets_bearer_tab_and_newline_after_scheme() {
608+
let r = redact_secrets("Authorization:\nBearer\tSECRETVALUE123456");
609+
assert!(
610+
!r.contains("SECRETVALUE123456"),
611+
"tab-after-Bearer must redact: {r}"
612+
);
613+
let r2 = redact_secrets("Bearer\nANOTHERSECRET987654");
614+
assert!(
615+
!r2.contains("ANOTHERSECRET987654"),
616+
"newline-after-Bearer must redact: {r2}"
617+
);
618+
let r3 = redact_secrets("Bearer firsttoken111111 and Bearer secondtoken222222");
619+
assert!(
620+
!r3.contains("firsttoken111111"),
621+
"first token must redact: {r3}"
622+
);
623+
assert!(
624+
!r3.contains("secondtoken222222"),
625+
"second token must redact: {r3}"
626+
);
627+
let r4 = redact_secrets("Basic\tdXNlcjpwYXNzd29yZA==");
628+
assert!(
629+
!r4.contains("dXNlcjpwYXNzd29yZA=="),
630+
"tab-after-Basic must redact: {r4}"
631+
);
632+
}
633+
595634
// =========================================================================
596635
// No secrets — passthrough
597636
// =========================================================================

0 commit comments

Comments
 (0)