aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/middleware/remote_ip.rb
blob: 79f9ddcd049b6d8cb58d01b62813f9170e622223 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
module ActionDispatch
  class RemoteIp
    class IpSpoofAttackError < StandardError ; end

    # IP addresses that are "trusted proxies" that can be stripped from
    # the comma-delimited list in the X-Forwarded-For header. See also:
    # http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
    TRUSTED_PROXIES = %r{
      ^127\.0\.0\.1$                | # localhost
      ^(10                          | # private IP 10.x.x.x
        172\.(1[6-9]|2[0-9]|3[0-1]) | # private IP in the range 172.16.0.0 .. 172.31.255.255
        192\.168                      # private IP 192.168.x.x
       )\.
    }x

    def initialize(app, check_ip_spoofing = true, custom_proxies = nil)
      @app = app
      @check_ip_spoofing = check_ip_spoofing
      if custom_proxies
        custom_regexp = Regexp.new(custom_proxies, "i")
        @trusted_proxies = Regexp.union(TRUSTED_PROXIES, custom_regexp)
      else
        @trusted_proxies = TRUSTED_PROXIES
      end
    end

    # Determines originating IP address. REMOTE_ADDR is the standard
    # but will be wrong if the user is behind a proxy. Proxies will set
    # HTTP_CLIENT_IP and/or HTTP_X_FORWARDED_FOR, so we prioritize those.
    # HTTP_X_FORWARDED_FOR may be a comma-delimited list in the case of
    # multiple chained proxies. The last address which is not a known proxy
    # will be the originating IP.
    def call(env)
      client_ip     = env['HTTP_CLIENT_IP']
      forwarded_ips = ips_from(env, 'HTTP_X_FORWARDED_FOR')
      remote_addrs  = ips_from(env, 'REMOTE_ADDR')

      if client_ip && @check_ip_spoofing && !forwarded_ips.include?(client_ip)
        # We don't know which came from the proxy, and which from the user
        raise IpSpoofAttackError, "IP spoofing attack?!" \
          "HTTP_CLIENT_IP=#{env['HTTP_CLIENT_IP'].inspect}" \
          "HTTP_X_FORWARDED_FOR=#{env['HTTP_X_FORWARDED_FOR'].inspect}"
      end

      remote_ip = client_ip || forwarded_ips.last || remote_addrs.last
      env["action_dispatch.remote_ip"] = remote_ip
      @app.call(env)
    end

  protected

    def ips_from(env, header)
      ips = env[header] ? env[header].strip.split(/[,\s]+/) : []
      ips.reject{|ip| ip =~ @trusted_proxies }
    end

  end
end