getsentry/raven-ruby

View on GitHub
sentry-ruby/lib/sentry/utils/real_ip.rb

Summary

Maintainability
A
25 mins
Test Coverage
# frozen_string_literal: true

require 'ipaddr'

# Based on ActionDispatch::RemoteIp. All security-related precautions from that
# middleware have been removed, because the Event IP just needs to be accurate,
# and spoofing an IP here only makes data inaccurate, not insecure. Don't re-use
# this module if you have to *trust* the IP address.
module Sentry
  module Utils
    class RealIp
      LOCAL_ADDRESSES = [
        "127.0.0.1",      # localhost IPv4
        "::1",            # localhost IPv6
        "fc00::/7",       # private IPv6 range fc00::/7
        "10.0.0.0/8",     # private IPv4 range 10.x.x.x
        "172.16.0.0/12",  # private IPv4 range 172.16.0.0 .. 172.31.255.255
        "192.168.0.0/16" # private IPv4 range 192.168.x.x
      ]

      attr_reader :ip

      def initialize(
        remote_addr: nil,
        client_ip: nil,
        real_ip: nil,
        forwarded_for: nil,
        trusted_proxies: []
      )
        @remote_addr = remote_addr
        @client_ip = client_ip
        @real_ip = real_ip
        @forwarded_for = forwarded_for
        @trusted_proxies = (LOCAL_ADDRESSES + Array(trusted_proxies)).map do |proxy|
          if proxy.is_a?(IPAddr)
            proxy
          else
            IPAddr.new(proxy.to_s)
          end
        end.uniq
      end

      def calculate_ip
        # CGI environment variable set by Rack
        remote_addr = ips_from(@remote_addr).last

        # Could be a CSV list and/or repeated headers that were concatenated.
        client_ips    = ips_from(@client_ip)
        real_ips      = ips_from(@real_ip)

        # The first address in this list is the original client, followed by
        # the IPs of successive proxies. We want to search starting from the end
        # until we find the first proxy that we do not trust.
        forwarded_ips = ips_from(@forwarded_for).reverse

        ips = [client_ips, real_ips, forwarded_ips, remote_addr].flatten.compact

        # If every single IP option is in the trusted list, just return REMOTE_ADDR
        @ip = filter_trusted_proxy_addresses(ips).first || remote_addr
      end

      protected

      def ips_from(header)
        # Split the comma-separated list into an array of strings
        ips = header ? header.strip.split(/[,\s]+/) : []
        ips.select do |ip|
          begin
            # Only return IPs that are valid according to the IPAddr#new method
            range = IPAddr.new(ip).to_range
            # we want to make sure nobody is sneaking a netmask in
            range.begin == range.end
          rescue ArgumentError
            nil
          end
        end
      end

      def filter_trusted_proxy_addresses(ips)
        ips.reject { |ip| @trusted_proxies.any? { |proxy| proxy === ip } }
      end
    end
  end
end