@@ -278,14 +278,40 @@ pub(crate) async fn deserialize_catalog_response<R: DeserializeOwned>(
278278 } )
279279}
280280
281+ /// Headers that contain sensitive information and should be excluded from logs.
282+ const SENSITIVE_HEADERS : & [ & str ] = & [
283+ "authorization" ,
284+ "proxy-authorization" ,
285+ "set-cookie" ,
286+ "cookie" ,
287+ "x-api-key" ,
288+ "x-auth-token" ,
289+ ] ;
290+
291+ /// Returns true if the header name is considered sensitive.
292+ fn is_sensitive_header ( name : & str ) -> bool {
293+ let name_lower = name. to_lowercase ( ) ;
294+ SENSITIVE_HEADERS . iter ( ) . any ( |h| name_lower == * h)
295+ }
296+
297+ /// Filters out sensitive headers and returns a debug-formatted string.
298+ fn format_headers_redacted ( headers : & HeaderMap ) -> String {
299+ let filtered: HashMap < & str , & str > = headers
300+ . iter ( )
301+ . filter ( |( name, _) | !is_sensitive_header ( name. as_str ( ) ) )
302+ . filter_map ( |( name, value) | value. to_str ( ) . ok ( ) . map ( |v| ( name. as_str ( ) , v) ) )
303+ . collect ( ) ;
304+ format ! ( "{:?}" , filtered)
305+ }
306+
281307/// Deserializes a unexpected catalog response into an error.
282308pub ( crate ) async fn deserialize_unexpected_catalog_error ( response : Response ) -> Error {
283309 let err = Error :: new (
284310 ErrorKind :: Unexpected ,
285311 "Received response with unexpected status code" ,
286312 )
287313 . with_context ( "status" , response. status ( ) . to_string ( ) )
288- . with_context ( "headers" , format ! ( "{:?}" , response. headers( ) ) ) ;
314+ . with_context ( "headers" , format_headers_redacted ( response. headers ( ) ) ) ;
289315
290316 let bytes = match response. bytes ( ) . await {
291317 Ok ( bytes) => bytes,
@@ -297,3 +323,99 @@ pub(crate) async fn deserialize_unexpected_catalog_error(response: Response) ->
297323 }
298324 err. with_context ( "json" , String :: from_utf8_lossy ( & bytes) )
299325}
326+
327+ #[ cfg( test) ]
328+ mod tests {
329+ use super :: * ;
330+
331+ #[ test]
332+ fn test_format_headers_redacted_empty ( ) {
333+ let headers = HeaderMap :: new ( ) ;
334+ let result = format_headers_redacted ( & headers) ;
335+ assert_eq ! ( result, "{}" ) ;
336+ }
337+
338+ #[ test]
339+ fn test_format_headers_redacted_non_sensitive ( ) {
340+ let mut headers = HeaderMap :: new ( ) ;
341+ headers. insert ( "content-type" , "application/json" . parse ( ) . unwrap ( ) ) ;
342+ headers. insert ( "x-request-id" , "abc123" . parse ( ) . unwrap ( ) ) ;
343+
344+ let result = format_headers_redacted ( & headers) ;
345+
346+ assert ! ( result. contains( "content-type" ) ) ;
347+ assert ! ( result. contains( "application/json" ) ) ;
348+ assert ! ( result. contains( "x-request-id" ) ) ;
349+ assert ! ( result. contains( "abc123" ) ) ;
350+ }
351+
352+ #[ test]
353+ fn test_format_headers_redacted_filters_sensitive ( ) {
354+ let mut headers = HeaderMap :: new ( ) ;
355+ headers. insert ( "authorization" , "Bearer secret-token" . parse ( ) . unwrap ( ) ) ;
356+ headers. insert ( "content-type" , "application/json" . parse ( ) . unwrap ( ) ) ;
357+
358+ let result = format_headers_redacted ( & headers) ;
359+
360+ // Sensitive header should be filtered out entirely
361+ assert ! ( !result. contains( "authorization" ) ) ;
362+ assert ! ( !result. contains( "secret-token" ) ) ;
363+ // Non-sensitive header should be present
364+ assert ! ( result. contains( "content-type" ) ) ;
365+ assert ! ( result. contains( "application/json" ) ) ;
366+ }
367+
368+ #[ test]
369+ fn test_format_headers_redacted_filters_set_cookie ( ) {
370+ let mut headers = HeaderMap :: new ( ) ;
371+ headers. insert (
372+ "set-cookie" ,
373+ "CF_Authorization=sensitive-session-token; Path=/; Secure;"
374+ . parse ( )
375+ . unwrap ( ) ,
376+ ) ;
377+ headers. insert ( "server" , "cloudflare" . parse ( ) . unwrap ( ) ) ;
378+
379+ let result = format_headers_redacted ( & headers) ;
380+
381+ // Sensitive header should be filtered out entirely
382+ assert ! ( !result. contains( "set-cookie" ) ) ;
383+ assert ! ( !result. contains( "sensitive-session-token" ) ) ;
384+ // Non-sensitive header should be present
385+ assert ! ( result. contains( "server" ) ) ;
386+ assert ! ( result. contains( "cloudflare" ) ) ;
387+ }
388+
389+ #[ test]
390+ fn test_format_headers_redacted_filters_all_sensitive ( ) {
391+ let mut headers = HeaderMap :: new ( ) ;
392+ headers. insert ( "authorization" , "Bearer token" . parse ( ) . unwrap ( ) ) ;
393+ headers. insert ( "proxy-authorization" , "Basic creds" . parse ( ) . unwrap ( ) ) ;
394+ headers. insert ( "set-cookie" , "session=abc" . parse ( ) . unwrap ( ) ) ;
395+ headers. insert ( "cookie" , "session=abc" . parse ( ) . unwrap ( ) ) ;
396+ headers. insert ( "x-api-key" , "api-key-123" . parse ( ) . unwrap ( ) ) ;
397+ headers. insert ( "x-auth-token" , "auth-token-456" . parse ( ) . unwrap ( ) ) ;
398+ headers. insert ( "x-request-id" , "req-123" . parse ( ) . unwrap ( ) ) ;
399+
400+ let result = format_headers_redacted ( & headers) ;
401+
402+ // All sensitive headers should be filtered out
403+ assert ! ( !result. contains( "authorization" ) ) ;
404+ assert ! ( !result. contains( "proxy-authorization" ) ) ;
405+ assert ! ( !result. contains( "set-cookie" ) ) ;
406+ assert ! ( !result. contains( "cookie" ) ) ;
407+ assert ! ( !result. contains( "x-api-key" ) ) ;
408+ assert ! ( !result. contains( "x-auth-token" ) ) ;
409+
410+ // Ensure no sensitive values leaked
411+ assert ! ( !result. contains( "Bearer token" ) ) ;
412+ assert ! ( !result. contains( "Basic creds" ) ) ;
413+ assert ! ( !result. contains( "session=abc" ) ) ;
414+ assert ! ( !result. contains( "api-key-123" ) ) ;
415+ assert ! ( !result. contains( "auth-token-456" ) ) ;
416+
417+ // Non-sensitive header should be present
418+ assert ! ( result. contains( "x-request-id" ) ) ;
419+ assert ! ( result. contains( "req-123" ) ) ;
420+ }
421+ }
0 commit comments