Skip to content

Commit 6e12d56

Browse files
committed
fix(secret): Add custom derive Debug for SecretResolver to prevent secret leakage with {:?}
Signed-off-by: Adrien Langou <alangou@nvidia.com>
1 parent 9ea94b6 commit 6e12d56

1 file changed

Lines changed: 76 additions & 1 deletion

File tree

crates/openshell-sandbox/src/secrets.rs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,26 @@ pub struct RewriteTargetResult {
8282
// SecretResolver
8383
// ---------------------------------------------------------------------------
8484

85-
#[derive(Debug, Clone, Default)]
85+
#[derive(Clone, Default)]
8686
pub struct SecretResolver {
8787
by_placeholder: HashMap<String, String>,
8888
}
8989

90+
// Manual `Debug` impl: the auto-derived `Debug` would format the
91+
// `by_placeholder` map, exposing both placeholder keys (which reveal which
92+
// provider env var names are configured) and the resolved secret values
93+
// themselves. Any accidental `{:?}` in a tracing call, or a
94+
// derived `Debug` on a containing struct, would write secrets to logs.
95+
//
96+
// We expose only the count of registered placeholders without leaking anything.
97+
impl fmt::Debug for SecretResolver {
98+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99+
f.debug_struct("SecretResolver")
100+
.field("placeholders", &self.by_placeholder.len())
101+
.finish_non_exhaustive() // Use to show that the struct is not empty
102+
}
103+
}
104+
90105
impl SecretResolver {
91106
#[cfg_attr(not(test), allow(dead_code))]
92107
pub(crate) fn from_provider_env(
@@ -1914,6 +1929,66 @@ mod tests {
19141929
assert_eq!(result.redacted, "/v1/chat/completions?format=json");
19151930
}
19161931

1932+
#[test]
1933+
fn debug_format_does_not_leak_secret_values() {
1934+
let (_, resolver) = SecretResolver::from_provider_env(
1935+
[
1936+
(
1937+
"ANTHROPIC_API_KEY".to_string(),
1938+
"sk-very-secret-value-12345".to_string(),
1939+
),
1940+
("DB_PASSWORD".to_string(), "very-secret-value".to_string()),
1941+
]
1942+
.into_iter()
1943+
.collect(),
1944+
);
1945+
let resolver = resolver.expect("resolver");
1946+
1947+
let plain = format!("{resolver:?}");
1948+
let pretty = format!("{resolver:#?}");
1949+
1950+
for output in [&plain, &pretty] {
1951+
assert!(
1952+
!output.contains("sk-very-secret-value-12345"),
1953+
"secret value leaked via Debug: {output}"
1954+
);
1955+
assert!(
1956+
!output.contains("very-secret-value"),
1957+
"secret value leaked via Debug: {output}"
1958+
);
1959+
assert!(
1960+
!output.contains("ANTHROPIC_API_KEY"),
1961+
"placeholder key (env var name) leaked via Debug: {output}"
1962+
);
1963+
assert!(
1964+
!output.contains("DB_PASSWORD"),
1965+
"placeholder key (env var name) leaked via Debug: {output}"
1966+
);
1967+
assert!(
1968+
!output.contains(PLACEHOLDER_PREFIX),
1969+
"placeholder prefix leaked via Debug: {output}"
1970+
);
1971+
assert!(
1972+
output.contains("SecretResolver"),
1973+
"Debug output should still identify the type: {output}"
1974+
);
1975+
}
1976+
1977+
assert!(
1978+
plain.contains('2'),
1979+
"Debug output should expose the placeholder count: {plain}"
1980+
);
1981+
}
1982+
1983+
#[test]
1984+
fn debug_format_of_empty_resolver_is_safe() {
1985+
let resolver = SecretResolver::default();
1986+
let output = format!("{resolver:?}");
1987+
assert!(output.contains("SecretResolver"));
1988+
assert!(output.contains('0'));
1989+
assert!(!output.contains(PLACEHOLDER_PREFIX));
1990+
}
1991+
19171992
#[test]
19181993
fn rewrite_target_for_eval_roundtrip() {
19191994
let (child_env, resolver) = SecretResolver::from_provider_env(

0 commit comments

Comments
 (0)