Skip to content

Commit 6e69dd0

Browse files
committed
fix(rest): Filter sensitive headers from error logs (#2117)
Sensitive headers like Set-Cookie, Authorization, and Cookie were being included in error messages, potentially exposing authentication tokens and session information in logs. This change filters out sensitive headers entirely from error context rather than logging their values, preventing credential leakage.
1 parent 5cec0f3 commit 6e69dd0

1 file changed

Lines changed: 123 additions & 1 deletion

File tree

crates/catalog/rest/src/client.rs

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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.
282308
pub(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

Comments
 (0)