@@ -82,11 +82,26 @@ pub struct RewriteTargetResult {
8282// SecretResolver
8383// ---------------------------------------------------------------------------
8484
85- #[ derive( Debug , Clone , Default ) ]
85+ #[ derive( Clone , Default ) ]
8686pub 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+
90105impl 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