File: ssrf_filter.rb

package info (click to toggle)
ruby-httpx 1.7.2-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,816 kB
  • sloc: ruby: 12,209; makefile: 4
file content (145 lines) | stat: -rw-r--r-- 4,941 bytes parent folder | download
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# frozen_string_literal: true

module HTTPX
  class ServerSideRequestForgeryError < Error; end

  module Plugins
    #
    # This plugin adds support for preventing Server-Side Request Forgery attacks.
    #
    # https://gitlab.com/os85/httpx/wikis/Server-Side-Request-Forgery-Filter
    #
    module SsrfFilter
      module IPAddrExtensions
        refine IPAddr do
          def prefixlen
            mask_addr = @mask_addr
            raise "Invalid mask" if mask_addr.zero?

            mask_addr >>= 1 while mask_addr.nobits?(0x1)

            length = 0
            while mask_addr & 0x1 == 0x1
              length += 1
              mask_addr >>= 1
            end

            length
          end
        end
      end

      using IPAddrExtensions

      # https://en.wikipedia.org/wiki/Reserved_IP_addresses
      IPV4_BLACKLIST = [
        IPAddr.new("0.0.0.0/8"), # Current network (only valid as source address)
        IPAddr.new("10.0.0.0/8"), # Private network
        IPAddr.new("100.64.0.0/10"), # Shared Address Space
        IPAddr.new("127.0.0.0/8"), # Loopback
        IPAddr.new("169.254.0.0/16"), # Link-local
        IPAddr.new("172.16.0.0/12"), # Private network
        IPAddr.new("192.0.0.0/24"), # IETF Protocol Assignments
        IPAddr.new("192.0.2.0/24"), # TEST-NET-1, documentation and examples
        IPAddr.new("192.88.99.0/24"), # IPv6 to IPv4 relay (includes 2002::/16)
        IPAddr.new("192.168.0.0/16"), # Private network
        IPAddr.new("198.18.0.0/15"), # Network benchmark tests
        IPAddr.new("198.51.100.0/24"), # TEST-NET-2, documentation and examples
        IPAddr.new("203.0.113.0/24"), # TEST-NET-3, documentation and examples
        IPAddr.new("224.0.0.0/4"), # IP multicast (former Class D network)
        IPAddr.new("240.0.0.0/4"), # Reserved (former Class E network)
        IPAddr.new("255.255.255.255"), # Broadcast
      ].freeze

      IPV6_BLACKLIST = ([
        IPAddr.new("::1/128"), # Loopback
        IPAddr.new("64:ff9b::/96"), # IPv4/IPv6 translation (RFC 6052)
        IPAddr.new("100::/64"), # Discard prefix (RFC 6666)
        IPAddr.new("2001::/32"), # Teredo tunneling
        IPAddr.new("2001:10::/28"), # Deprecated (previously ORCHID)
        IPAddr.new("2001:20::/28"), # ORCHIDv2
        IPAddr.new("2001:db8::/32"), # Addresses used in documentation and example source code
        IPAddr.new("2002::/16"), # 6to4
        IPAddr.new("fc00::/7"), # Unique local address
        IPAddr.new("fe80::/10"), # Link-local address
        IPAddr.new("ff00::/8"), # Multicast
      ] + IPV4_BLACKLIST.flat_map do |ipaddr|
        prefixlen = ipaddr.prefixlen

        ipv4_compatible = ipaddr.ipv4_compat.mask(96 + prefixlen)
        ipv4_mapped = ipaddr.ipv4_mapped.mask(80 + prefixlen)

        [ipv4_compatible, ipv4_mapped]
      end).freeze

      class << self
        def extra_options(options)
          options.merge(allowed_schemes: %w[https http])
        end

        def unsafe_ip_address?(ipaddr)
          range = ipaddr.to_range
          return true if range.first != range.last

          return IPV6_BLACKLIST.any? { |r| r.include?(ipaddr) } if ipaddr.ipv6?

          IPV4_BLACKLIST.any? { |r| r.include?(ipaddr) } # then it's IPv4
        end
      end

      # adds support for the following options:
      #
      # :allowed_schemes :: list of URI schemes allowed (defaults to <tt>["https", "http"]</tt>)
      module OptionsMethods
        private

        def option_allowed_schemes(value)
          Array(value)
        end
      end

      module InstanceMethods
        def send_requests(*requests)
          responses = requests.map do |request|
            next if @options.allowed_schemes.include?(request.uri.scheme)

            error = ServerSideRequestForgeryError.new("#{request.uri} URI scheme not allowed")
            error.set_backtrace(caller)
            response = ErrorResponse.new(request, error)
            request.emit(:response, response)
            response
          end
          allowed_requests = requests.select { |req| responses[requests.index(req)].nil? }
          allowed_responses = super(*allowed_requests)
          allowed_responses.each_with_index do |res, idx|
            req = allowed_requests[idx]
            responses[requests.index(req)] = res
          end

          responses
        end
      end

      module ConnectionMethods
        def initialize(*)
          begin
            super
          rescue ServerSideRequestForgeryError => e
            # may raise when IPs are passed as options via :addresses
            throw(:resolve_error, e)
          end
        end

        def addresses=(addrs)
          addrs.reject!(&SsrfFilter.method(:unsafe_ip_address?))

          raise ServerSideRequestForgeryError, "#{@origin.host} has no public IP addresses" if addrs.empty?

          super
        end
      end
    end

    register_plugin :ssrf_filter, SsrfFilter
  end
end