Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

- Restored the reqwest transport's pre-0.13 protocol features by disabling HTTP/2 and native-TLS ALPN ([#1258](https://github.com/getsentry/sentry-rust/pull/1258)).
- [`EnvelopeError`](https://docs.rs/sentry-types/0.49.0/sentry_types/protocol/envelope/enum.EnvelopeError.html) is now `#[non_exhaustive]` to allow adding new error variants without a breaking change ([#1254](https://github.com/getsentry/sentry-rust/pull/1254)).
- Captured client IP addresses in the tower HTTP integration when default PII is enabled ([#1274](https://github.com/getsentry/sentry-rust/pull/1274)).

## 0.48.5

Expand Down
148 changes: 131 additions & 17 deletions sentry-tower/src/http.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::convert::TryInto;
use std::future::Future;
use std::net::{IpAddr, SocketAddr};
use std::pin::Pin;
use std::task::{Context, Poll};

Expand Down Expand Up @@ -185,23 +186,7 @@ where
}

fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
let sentry_req = sentry_core::protocol::Request {
method: Some(request.method().to_string()),
url: get_url_from_request(&request).map(scrub_pii_from_url),
headers: request
.headers()
.into_iter()
.filter(|(_, value)| !value.is_sensitive())
.filter(|(header, _)| self.with_pii || !is_sensitive_header(header.as_str()))
.map(|(header, value)| {
(
header.to_string(),
value.to_str().unwrap_or_default().into(),
)
})
.collect(),
..Default::default()
};
let sentry_req = sentry_request_from_http(&request, self.with_pii);
let trx_ctx = if self.start_transaction {
let headers = request.headers().into_iter().flat_map(|(header, value)| {
value.to_str().ok().map(|value| (header.as_str(), value))
Expand All @@ -224,6 +209,65 @@ where
}
}

fn sentry_request_from_http<B>(request: &Request<B>, with_pii: bool) -> protocol::Request {
let mut sentry_req = protocol::Request {
method: Some(request.method().to_string()),
url: get_url_from_request(request).map(scrub_pii_from_url),
headers: request
.headers()
.into_iter()
.filter(|(_, value)| !value.is_sensitive())
.filter(|(header, _)| with_pii || !is_sensitive_header(header.as_str()))
.map(|(header, value)| {
(
header.to_string(),
value.to_str().unwrap_or_default().into(),
)
})
.collect(),
..Default::default()
};

if with_pii {
if let Some(remote_addr) = remote_addr_from_request(request) {
sentry_req.env.insert("REMOTE_ADDR".into(), remote_addr);
}
}

sentry_req
}

fn remote_addr_from_request<B>(request: &Request<B>) -> Option<String> {
request
.headers()
.get("x-forwarded-for")
.and_then(|value| value.to_str().ok())
.and_then(|value| value.split(',').next())
.map(str::trim)
.filter(|value| !value.is_empty())
.or_else(|| {
request
.headers()
.get("x-real-ip")
.and_then(|value| value.to_str().ok())
.map(str::trim)
.filter(|value| !value.is_empty())
})
Comment on lines +245 to +255

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The remote_addr_from_request function extracts an IP from the x-forwarded-for header without validating if the value is a syntactically correct IP address, potentially capturing and sending invalid data.
Severity: MEDIUM

Suggested Fix

Update the remote_addr_from_request function to validate that the extracted string is a valid IPv4 or IPv6 address before returning it. This can be achieved by parsing the string using a standard library function like std::net::IpAddr::from_str. This will ensure only valid IP addresses are processed.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: sentry-tower/src/http.rs#L243-L255

Potential issue: The `remote_addr_from_request` function extracts a potential IP address
from the `x-forwarded-for` header. It processes the header value but does not validate
if the resulting string is a syntactically correct IP address. The only check performed
is to ensure the string is not empty after trimming. This allows malformed values, such
as `not-an-ip`, or even malicious content to be captured and sent to Sentry as the
`REMOTE_ADDR`. While the Sentry backend may perform its own validation, this behavior
allows invalid data from an untrusted source to be processed and transmitted over the
network.

Did we get this right? 👍 / 👎 to inform future reviews.

.map(ToOwned::to_owned)
.or_else(|| {
request
.extensions()
.get::<SocketAddr>()
.map(|address| address.ip().to_string())
})
.or_else(|| {
request
.extensions()
.get::<IpAddr>()
.map(ToString::to_string)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Axum peer IP lookup fails

Medium Severity

The socket fallback looks up bare SocketAddr and IpAddr in request extensions, but axum stores the peer address as ConnectInfo&lt;SocketAddr&gt;. With the documented axum setup—and especially axum::serve, which provides that extension by default—the fallback never finds a client IP when proxy headers are absent, so REMOTE_ADDR is left unset.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5f80b05. Configure here.

}

fn path_from_request<B>(request: &Request<B>) -> &str {
#[cfg(feature = "axum-matched-path")]
if let Some(matched_path) = request.extensions().get::<axum::extract::MatchedPath>() {
Expand Down Expand Up @@ -260,3 +304,73 @@ fn get_url_from_request<B>(request: &Request<B>) -> Option<url::Url> {
let uri = uri::Uri::from_parts(uri_parts).ok()?;
uri.to_string().parse().ok()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn captures_first_forwarded_address_with_pii() {
let request = Request::builder()
.header("host", "example.com")
.header("x-forwarded-for", " 203.0.113.9, 10.0.0.1")
.body(())
.unwrap();

let sentry_req = sentry_request_from_http(&request, true);

assert_eq!(
sentry_req.env.get("REMOTE_ADDR").map(String::as_str),
Some("203.0.113.9")
);
}

#[test]
fn captures_real_ip_when_forwarded_address_is_empty() {
let request = Request::builder()
.header("host", "example.com")
.header("x-forwarded-for", " ")
.header("x-real-ip", "198.51.100.7")
.body(())
.unwrap();

let sentry_req = sentry_request_from_http(&request, true);

assert_eq!(
sentry_req.env.get("REMOTE_ADDR").map(String::as_str),
Some("198.51.100.7")
);
}

#[test]
fn captures_socket_address_when_proxy_headers_are_absent() {
let mut request = Request::builder()
.header("host", "example.com")
.body(())
.unwrap();
request
.extensions_mut()
.insert("192.0.2.4:8080".parse::<SocketAddr>().unwrap());

let sentry_req = sentry_request_from_http(&request, true);

assert_eq!(
sentry_req.env.get("REMOTE_ADDR").map(String::as_str),
Some("192.0.2.4")
);
}

#[test]
fn omits_remote_address_without_pii() {
let request = Request::builder()
.header("host", "example.com")
.header("x-forwarded-for", "203.0.113.9")
.body(())
.unwrap();

let sentry_req = sentry_request_from_http(&request, false);

assert!(!sentry_req.env.contains_key("REMOTE_ADDR"));
assert!(!sentry_req.headers.contains_key("x-forwarded-for"));
}
}