Skip to content

Let apps choose how the client IP on events is determined #3180

Open

Description

@dentarg

Problem Statement

With send_default_pii (data_collection.user_info in 7.x), Event#rack_env= sets user[:ip_address] from Utils::RealIp. It puts Client-IP and X-Real-IP first, then X-Forwarded-For from the right, then REMOTE_ADDR, and takes the first address that isn't a trusted proxy.

Some platforms pass those headers through from the client. Heroku's router, for one, appends the address it saw to X-Forwarded-For and passes Client-IP and X-Real-IP on as the client sent them. A client that sends Client-IP: 198.51.100.2 gets that address on its events, and the address the router saw is never used. trusted_proxies can't help: it only removes the listed proxies' addresses from those candidates, and the Client-IP value is whatever the client wrote, not a proxy, so it stays first.

real_ip.rb says spoofing "only makes data inaccurate, not insecure", and accuracy is the problem here: on such a platform the IP on an event is whatever the client chose. Only the app knows its topology, and so which header to read: the last X-Forwarded-For entry on Heroku, Fly-Client-IP on Fly.io, CF-Connecting-IP behind Cloudflare.

The only way we found that keeps the SDK's own rules is to override the private Event#calculate_real_ip_from_rack(env):

module ClientIP
  private

  def calculate_real_ip_from_rack(env)
    env["HTTP_X_FORWARDED_FOR"]&.split(",")&.last&.strip # Heroku
  end
end
Sentry::Event.prepend(ClientIP)

The SDK still decides when an event gets an address, and keeps one set with Sentry.set_user (#2350). But the method is private: a rename quietly brings back the header-based address, and a new argument makes every event from a web request fail to build, so capture_event drops it. The public alternatives fall short. A global event processor has to repeat the SDK's rules for each version, and in 5.x it also receives LogEvents, which have no request. Sentry.set_user(ip_address: ...) is replaced by any later set_user call in the app.

Solution Brainstorm

A supported way for the app to supply the address, called where calculate_real_ip_from_rack is today, so the SDK's checks stay as they are. For example, a callable:

Sentry.init do |config|
  config.send_default_pii = true
  # Called with the Rack env when the SDK adds the user's IP address
  config.real_ip = ->(env) { env["HTTP_X_FORWARDED_FOR"]&.split(",")&.last&.strip }
end

That would also cover #2592: an app whose clients are on a private network could return their address without changing how trusted_proxies and LOCAL_ADDRESSES work for everyone else. A narrower option that picks the headers RealIp reads would fix the precedence, but not #2592.

We checked sentry-ruby 5.25, 5.28, 6.7 and 7.2, which all work out the address this way.

Activity

  1. linear-code commented on Nov 4, 2026

    @linear-code
  2. sl0thentr0py commented on Nov 4, 2026

    @sl0thentr0py
    Member

    thx @dentarg, yea we can certainly add a callback for this

  3. self-assigned this
    on Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions