Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions crates/tinyinference-core/src/sanitize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,14 @@ pub fn scrub_secret_patterns(input: &str) -> String {
/// [`scrub_credentials`] drops `==` comparisons and asks [`looks_like_secret`]
/// whether the value is a credential or ordinary code.
///
/// Groups: `key` = sensitive key, 2 = operator, 3 = double-quoted value,
/// 4 = single-quoted value, 5 = bare value. A bare value may start with `$`
/// and continue through `@ $ % # ~ ^`; `!`, `?`, `:`, `&` and `*` still end it
/// (GraphQL `String!`, `Token::Bearer`, query strings, the redaction marker).
/// Groups: `key` = sensitive key, `op` = operator, `dq`/`sq` = double/single-
/// quoted value, `edq` = value quoted with `\"` inside stringified JSON
/// (a value holding an escaped quote is only redacted up to it), `bare` =
/// bare value. A bare value may start with `$` and continue through
/// `@ $ % # ~ ^`; `!`, `?`, `:`, `&` and `*` still end it (GraphQL `String!`,
/// `Token::Bearer`, query strings, the redaction marker).
static SENSITIVE_KV_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"(?i)\b(?P<key>[a-z0-9_\-]*(?:token|api[_-]?key|password|secret|user[_-]?key|bearer|credential))["']?[ \t]*(:=|==|[:=])[ \t]*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([a-zA-Z0-9_+./=\-$][a-zA-Z0-9_+./=\-@$%#~^]*))"#).unwrap()
Regex::new(r#"(?i)\b(?P<key>[a-z0-9_\-]*(?:token|api[_-]?key|password|secret|user[_-]?key|bearer|credential))(?:\\?["'])?[ \t]*(?P<op>:=|==|[:=])[ \t]*(?:"(?P<dq>(?:\\.|[^"\\])*)"|'(?P<sq>(?:\\.|[^'\\])*)'|\\"(?P<edq>(?:\\\\|[^"\\])*)\\"|(?P<bare>[a-zA-Z0-9_+./=\-$][a-zA-Z0-9_+./=\-@$%#~^]*))"#).unwrap()
});

/// Value prefixes issued by credential providers. A labelled value starting
Expand Down Expand Up @@ -298,14 +300,15 @@ pub fn scrub_credentials(input: &str) -> String {
let stage_kv = SENSITIVE_KV_REGEX.replace_all(input, |caps: &regex::Captures<'_>| {
let full_match = &caps[0];
// `token == other` compares; it does not assign.
if &caps[2] == "==" {
if &caps["op"] == "==" {
return full_match.to_string();
}
let quoted = caps.get(5).is_none();
let quoted = caps.name("bare").is_none();
let value = caps
.get(3)
.or(caps.get(4))
.or(caps.get(5))
.name("dq")
.or(caps.name("sq"))
.or(caps.name("edq"))
.or(caps.name("bare"))
.expect("sensitive key-value match has a value");
let rest = &input[caps.get(0).expect("full match").end()..];
let key = caps.name("key").expect("sensitive key capture");
Expand All @@ -319,7 +322,7 @@ pub fn scrub_credentials(input: &str) -> String {
let annotation_follows = rest
.trim_start_matches([' ', '\t'])
.starts_with([',', ';', ')', '<', '>', '|', '=', '[', '{', '?']);
if &caps[2] == ":"
if &caps["op"] == ":"
&& !quoted
&& ((value
.as_str()
Expand Down
8 changes: 8 additions & 0 deletions crates/tinyinference-core/src/sanitize_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,11 @@ fn bare_values_with_punctuation_are_fully_redacted() {
.collect();
assert!(leaked.is_empty(), "leaked: {leaked:?}");
}

// A labelled secret inside stringified (escaped) JSON.
#[test]
fn secret_in_escaped_json_is_redacted() {
let input = r#"{"body":"{\"password\":\"hunter2secret\"}"}"#;
let out = scrub_credentials(input);
assert!(!out.contains("hunter2secret"), "leaked: {out}");
}
Loading