Skip to content

Commit 5b4a75d

Browse files
committed
enhancement(http_request): add redaction for sensitive headers
1 parent bddc786 commit 5b4a75d

File tree

2 files changed

+105
-4
lines changed

2 files changed

+105
-4
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The `http_request` function now redacts sensitive header values in error messages to prevent credential leakage in logs. Headers such as `Authorization`, `Cookie`, `X-Api-Key`, and any header containing "token", "secret", or "password" will now display as `***` in error output, while non-sensitive headers remain visible for debugging purposes.

src/stdlib/http_request.rs

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)