Skip to content

Commit 8ee7d02

Browse files
authored
fix(sentry): redact sensitive request header values in captured events (#2432)
fix(sentry): redact sensitive request header values in captured events
1 parent bd3275e commit 8ee7d02

2 files changed

Lines changed: 113 additions & 1 deletion

File tree

syncserver-common/src/middleware/sentry.rs

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,46 @@ where
173173
builder.send();
174174
}
175175

176+
/// Request headers whose values may contain credentials or user-identifying
177+
/// data and so must not be recorded on Sentry events. Compared case-insensitively.
178+
const REDACTED_HEADERS: &[&str] = &[
179+
"authorization",
180+
"proxy-authorization",
181+
"cookie",
182+
"x-client-state",
183+
"x-keyid",
184+
"x-forwarded-for",
185+
];
186+
187+
/// Placeholder substituted for the value of a [`REDACTED_HEADERS`] header.
188+
const REDACTED_HEADER_VALUE: &str = "[redacted]";
189+
190+
/// Return the header value to record on a Sentry event, substituting a
191+
/// placeholder for headers whose values may carry credentials or
192+
/// user-identifying data.
193+
fn sentry_header_value(name: &str, value: &str) -> String {
194+
if REDACTED_HEADERS.contains(&name.to_ascii_lowercase().as_str()) {
195+
REDACTED_HEADER_VALUE.to_owned()
196+
} else {
197+
value.to_owned()
198+
}
199+
}
200+
201+
/// Redact sensitive request-header values on a Sentry event in place.
202+
///
203+
/// Intended to be called from a `before_send` hook so that every event is
204+
/// scrubbed before transmission — regardless of where it originates — not only
205+
/// requests built by [`sentry_request_from_http`].
206+
pub fn scrub_event_request_headers(event: &mut Event<'static>) {
207+
if let Some(request) = event.request.as_mut() {
208+
for (name, value) in request.headers.iter_mut() {
209+
if REDACTED_HEADERS.contains(&name.to_ascii_lowercase().as_str()) {
210+
*value = REDACTED_HEADER_VALUE.to_owned();
211+
}
212+
}
213+
}
214+
}
215+
176216
/// Build a Sentry request struct from the HTTP request
177217
fn sentry_request_from_http(request: &ServiceRequest) -> sentry::protocol::Request {
178218
sentry::protocol::Request {
@@ -188,7 +228,13 @@ fn sentry_request_from_http(request: &ServiceRequest) -> sentry::protocol::Reque
188228
headers: request
189229
.headers()
190230
.iter()
191-
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or_default().to_string()))
231+
.map(|(k, v)| {
232+
let name = k.as_str();
233+
(
234+
name.to_owned(),
235+
sentry_header_value(name, v.to_str().unwrap_or_default()),
236+
)
237+
})
192238
.collect(),
193239
..Default::default()
194240
}
@@ -344,3 +390,60 @@ fn resolved_backtrace(err: &dyn ReportableError) -> Option<Backtrace> {
344390
}
345391
backtrace
346392
}
393+
394+
#[cfg(test)]
395+
mod tests {
396+
use super::{REDACTED_HEADER_VALUE, scrub_event_request_headers, sentry_header_value};
397+
398+
#[test]
399+
fn before_send_scrub_redacts_request_headers() {
400+
use sentry::protocol::{Event, Request};
401+
402+
let mut headers = std::collections::BTreeMap::new();
403+
headers.insert("Authorization".to_owned(), "Bearer secret".to_owned());
404+
headers.insert("X-KeyID".to_owned(), "kid-value".to_owned());
405+
headers.insert("content-type".to_owned(), "application/json".to_owned());
406+
407+
let mut event = Event {
408+
request: Some(Request {
409+
headers,
410+
..Default::default()
411+
}),
412+
..Default::default()
413+
};
414+
scrub_event_request_headers(&mut event);
415+
416+
let headers = event.request.unwrap().headers;
417+
assert_eq!(headers["Authorization"], REDACTED_HEADER_VALUE);
418+
assert_eq!(headers["X-KeyID"], REDACTED_HEADER_VALUE);
419+
assert_eq!(headers["content-type"], "application/json");
420+
}
421+
422+
#[test]
423+
fn sensitive_header_values_are_redacted() {
424+
// Sensitive headers are redacted regardless of case.
425+
for name in [
426+
"authorization",
427+
"Authorization",
428+
"Cookie",
429+
"X-KeyID",
430+
"x-client-state",
431+
"X-Forwarded-For",
432+
] {
433+
assert_eq!(
434+
sentry_header_value(name, "sensitive-value"),
435+
REDACTED_HEADER_VALUE,
436+
"expected `{name}` to be redacted"
437+
);
438+
}
439+
}
440+
441+
#[test]
442+
fn ordinary_header_values_pass_through() {
443+
assert_eq!(
444+
sentry_header_value("content-type", "application/json"),
445+
"application/json"
446+
);
447+
assert_eq!(sentry_header_value("user-agent", "Firefox"), "Firefox");
448+
}
449+
}

syncserver/src/main.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
4040
// Note: set "debug: true," to diagnose sentry issues
4141
release: sentry::release_name!(),
4242
environment: Some(settings.environment.clone().into()),
43+
// Scrub sensitive request headers from every event before it is sent,
44+
// as a central complement to the per-request filtering in the Sentry
45+
// middleware (covers events emitted from any source).
46+
before_send: Some(std::sync::Arc::new(
47+
|mut event: sentry::protocol::Event<'static>| {
48+
syncserver_common::middleware::sentry::scrub_event_request_headers(&mut event);
49+
Some(event)
50+
},
51+
)),
4352
..sentry::ClientOptions::default()
4453
});
4554
opts.integrations.retain(|i| i.name() != "debug-images");

0 commit comments

Comments
 (0)