Skip to content

Commit 0051b28

Browse files
committed
fix(sandbox): redact sensitive structured log values
1 parent 6828e14 commit 0051b28

File tree

1 file changed

+131
-4
lines changed

1 file changed

+131
-4
lines changed

crates/openshell-sandbox/src/log_push.rs

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,61 @@ async fn drain_during_backoff(
243243
}
244244
}
245245

246+
const REDACTED_LOG_VALUE: &str = "[REDACTED]";
247+
248+
fn sanitize_field_value(field_name: &str, value: &str) -> String {
249+
if field_name_looks_sensitive(field_name) || value_looks_sensitive(value) {
250+
REDACTED_LOG_VALUE.to_string()
251+
} else {
252+
value.to_string()
253+
}
254+
}
255+
256+
fn field_name_looks_sensitive(field_name: &str) -> bool {
257+
let normalized = field_name.to_ascii_lowercase();
258+
matches!(
259+
normalized.as_str(),
260+
"authorization"
261+
| "proxy_authorization"
262+
| "token"
263+
| "secret"
264+
| "password"
265+
| "passwd"
266+
| "api_key"
267+
| "apikey"
268+
) || matches!(
269+
normalized.as_str(),
270+
name if name.ends_with("_token")
271+
|| name.ends_with("_secret")
272+
|| name.ends_with("_password")
273+
|| name.ends_with("_passwd")
274+
|| name.ends_with("_api_key")
275+
|| name.ends_with("_apikey")
276+
)
277+
}
278+
279+
fn value_looks_sensitive(value: &str) -> bool {
280+
let candidate = strip_wrapping_quotes(value.trim());
281+
let lower = candidate.to_ascii_lowercase();
282+
lower.starts_with("bearer ")
283+
|| lower.starts_with("openshell:resolve:")
284+
|| candidate.starts_with("sk-")
285+
}
286+
287+
fn strip_wrapping_quotes(mut value: &str) -> &str {
288+
loop {
289+
let trimmed = value.trim();
290+
if trimmed.len() >= 2
291+
&& ((trimmed.starts_with('"') && trimmed.ends_with('"'))
292+
|| (trimmed.starts_with('\'') && trimmed.ends_with('\'')))
293+
{
294+
value = &trimmed[1..trimmed.len() - 1];
295+
continue;
296+
}
297+
return trimmed;
298+
}
299+
}
300+
246301
#[derive(Debug, Default)]
247302
struct LogVisitor {
248303
message: Option<String>,
@@ -263,17 +318,22 @@ impl tracing::field::Visit for LogVisitor {
263318
if field.name() == "message" {
264319
self.message = Some(value.to_string());
265320
} else {
266-
self.fields
267-
.push((field.name().to_string(), value.to_string()));
321+
self.fields.push((
322+
field.name().to_string(),
323+
sanitize_field_value(field.name(), value),
324+
));
268325
}
269326
}
270327

271328
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
272329
if field.name() == "message" {
273330
self.message = Some(format!("{value:?}"));
274331
} else {
275-
self.fields
276-
.push((field.name().to_string(), format!("{value:?}")));
332+
let rendered = format!("{value:?}");
333+
self.fields.push((
334+
field.name().to_string(),
335+
sanitize_field_value(field.name(), &rendered),
336+
));
277337
}
278338
}
279339
}
@@ -282,3 +342,70 @@ fn current_time_ms() -> Option<i64> {
282342
let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?;
283343
i64::try_from(now.as_millis()).ok()
284344
}
345+
346+
#[cfg(test)]
347+
mod tests {
348+
use super::*;
349+
350+
#[test]
351+
fn sanitize_field_value_redacts_sensitive_field_names() {
352+
assert_eq!(
353+
sanitize_field_value("authorization", "Basic abc123"),
354+
REDACTED_LOG_VALUE
355+
);
356+
assert_eq!(
357+
sanitize_field_value("api_key", "not-a-pattern-match"),
358+
REDACTED_LOG_VALUE
359+
);
360+
assert_eq!(
361+
sanitize_field_value("session_token", "opaque"),
362+
REDACTED_LOG_VALUE
363+
);
364+
}
365+
366+
#[test]
367+
fn sanitize_field_value_redacts_known_secret_prefixes() {
368+
assert_eq!(
369+
sanitize_field_value("dst_host", "Bearer abc123"),
370+
REDACTED_LOG_VALUE
371+
);
372+
assert_eq!(
373+
sanitize_field_value("dst_host", "sk-proj-123456"),
374+
REDACTED_LOG_VALUE
375+
);
376+
assert_eq!(
377+
sanitize_field_value("dst_host", "openshell:resolve:provider.token"),
378+
REDACTED_LOG_VALUE
379+
);
380+
}
381+
382+
#[test]
383+
fn sanitize_field_value_redacts_debug_quoted_secret_values() {
384+
assert_eq!(
385+
sanitize_field_value("metadata", "\"Bearer abc123\""),
386+
REDACTED_LOG_VALUE
387+
);
388+
assert_eq!(
389+
sanitize_field_value("metadata", "\"sk-secret-value\""),
390+
REDACTED_LOG_VALUE
391+
);
392+
}
393+
394+
#[test]
395+
fn sanitize_field_value_preserves_benign_fields() {
396+
assert_eq!(
397+
sanitize_field_value("l7_target", "api.openai.com"),
398+
"api.openai.com"
399+
);
400+
assert_eq!(sanitize_field_value("token_count", "42"), "42");
401+
assert_eq!(
402+
sanitize_field_value("event", "BearerTokenParsingFailed"),
403+
"BearerTokenParsingFailed"
404+
);
405+
}
406+
407+
#[test]
408+
fn current_time_ms_returns_some() {
409+
assert!(current_time_ms().is_some());
410+
}
411+
}

0 commit comments

Comments
 (0)