@@ -13,6 +13,7 @@ mod non_wasm {
1313 Context , Expression , ExpressionError , FunctionExpression , Resolved , TypeDef , TypeState ,
1414 Value , VrlValueConvert ,
1515 } ;
16+ use crate :: value:: value:: ObjectMap ;
1617 use reqwest_middleware:: {
1718 ClientBuilder , ClientWithMiddleware ,
1819 reqwest:: {
@@ -50,6 +51,36 @@ mod non_wasm {
5051 . build ( )
5152 }
5253
54+ /// Redacts sensitive header values to prevent them from appearing in error messages.
55+ /// Headers like Authorization, Cookie, and API keys are replaced with ***.
56+ fn redact_sensitive_headers ( headers : & ObjectMap ) -> ObjectMap {
57+ const SENSITIVE_HEADERS : & [ & str ] = & [
58+ "authorization" ,
59+ "cookie" ,
60+ "set-cookie" ,
61+ "x-api-key" ,
62+ "api-key" ,
63+ "x-auth-token" ,
64+ "proxy-authorization" ,
65+ ] ;
66+
67+ headers
68+ . iter ( )
69+ . map ( |( key, value) | {
70+ let key_lower = key. as_ref ( ) . to_lowercase ( ) ;
71+ if SENSITIVE_HEADERS . contains ( & key_lower. as_str ( ) )
72+ || key_lower. contains ( "token" )
73+ || key_lower. contains ( "secret" )
74+ || key_lower. contains ( "password" )
75+ {
76+ ( key. clone ( ) , Value :: from ( "***" ) )
77+ } else {
78+ ( key. clone ( ) , value. clone ( ) )
79+ }
80+ } )
81+ . collect ( )
82+ }
83+
5384 async fn http_request (
5485 client : & ClientWithMiddleware ,
5586 url : & Value ,
@@ -62,6 +93,8 @@ mod non_wasm {
6293 let headers = headers. try_object ( ) ?;
6394 let body = body. try_bytes_utf8_lossy ( ) ?;
6495
96+ let redacted_headers = redact_sensitive_headers ( & headers) ;
97+
6598 let method = Method :: try_from ( method. as_str ( ) )
6699 . map_err ( |_| format ! ( "Unsupported HTTP method: {method}" ) ) ?;
67100 let mut header_map = HeaderMap :: new ( ) ;
@@ -71,19 +104,39 @@ mod non_wasm {
71104 . parse :: < HeaderName > ( )
72105 . map_err ( |_| format ! ( "Invalid header key: {key}" ) ) ?;
73106 let val = value
74- . try_bytes_utf8_lossy ( ) ?
107+ . try_bytes_utf8_lossy ( )
108+ . map_err ( |e| {
109+ format ! (
110+ "Invalid header value for key '{key}': {} (headers: {})" ,
111+ e,
112+ Value :: Object ( redacted_headers. clone( ) )
113+ )
114+ } ) ?
75115 . parse :: < HeaderValue > ( )
76- . map_err ( |_| format ! ( "Invalid header value: {value}" ) ) ?;
116+ . map_err ( |_| {
117+ format ! (
118+ "Invalid header value for key '{key}' (headers: {})" ,
119+ Value :: Object ( redacted_headers. clone( ) )
120+ )
121+ } ) ?;
77122 header_map. insert ( key, val) ;
78123 }
79124
80125 let response = client
81- . request ( method, url. as_ref ( ) )
126+ . request ( method. clone ( ) , url. as_ref ( ) )
82127 . headers ( header_map)
83128 . body ( body. as_bytes ( ) . to_owned ( ) )
84129 . send ( )
85130 . await
86- . map_err ( |e| format ! ( "HTTP request failed: {e}" ) ) ?;
131+ . map_err ( |e| {
132+ format ! (
133+ "HTTP request failed: {} (url: {}, method: {}, headers: {})" ,
134+ e,
135+ url,
136+ method,
137+ Value :: Object ( redacted_headers. clone( ) )
138+ )
139+ } ) ?;
87140
88141 let body = response
89142 . text ( )
@@ -466,4 +519,51 @@ mod tests {
466519 let error = result. unwrap_err ( ) ;
467520 assert ! ( error. to_string( ) . contains( "Invalid proxy" ) ) ;
468521 }
522+
523+ #[ tokio:: test( flavor = "multi_thread" ) ]
524+ async fn test_sensitive_headers_redacted ( ) {
525+ let func = HttpRequestFn {
526+ url : expr ! ( "not-a-valid-url" ) ,
527+ method : expr ! ( "get" ) ,
528+ headers : expr ! ( {
529+ "Authorization" : "Bearer super_secret_12345" ,
530+ "X-Api-Key" : "key-67890" ,
531+ "Content-Type" : "application/json" ,
532+ "Cookie" : "session=abcdef" ,
533+ "X-Custom-Token" : "another-secret" ,
534+ "User-Agent" : "VRL/0.28"
535+ } ) ,
536+ body : expr ! ( "" ) ,
537+ client_or_proxies : ClientOrProxies :: no_proxies ( ) ,
538+ } ;
539+
540+ let result = execute_http_request ( & func) ;
541+ assert ! ( result. is_err( ) ) ;
542+ let error = result. unwrap_err ( ) . to_string ( ) ;
543+
544+ // Verify that sensitive values are redacted
545+ assert ! (
546+ !error. contains( "super_secret_12345" ) ,
547+ "Authorization token should be redacted"
548+ ) ;
549+ assert ! ( !error. contains( "key-67890" ) , "API key should be redacted" ) ;
550+ assert ! ( !error. contains( "abcdef" ) , "Cookie should be redacted" ) ;
551+ assert ! (
552+ !error. contains( "another-secret" ) ,
553+ "Custom token should be redacted"
554+ ) ;
555+
556+ // Verify that redacted placeholder appears
557+ assert ! ( error. contains( "***" ) , "Should contain *** placeholder" ) ;
558+
559+ // Verify that non-sensitive headers are still visible
560+ assert ! (
561+ error. contains( "application/json" ) ,
562+ "Non-sensitive headers should not be redacted"
563+ ) ;
564+ assert ! (
565+ error. contains( "VRL/0.28" ) ,
566+ "User-Agent should not be redacted"
567+ ) ;
568+ }
469569}
0 commit comments