ssrf_filter 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 68db49bb250c05d11655c0afe1913b34247d783c
4
+ data.tar.gz: f1d11ee2913e121b3c996a1af745926a3da953e2
5
+ SHA512:
6
+ metadata.gz: 201bf113012c32ddb97e79df1fe9428d9dff43be894d340789a6b937f819e4538e680f8ec66e0d458a396ed7e082d1d20374d4b8c0fc0833970fb6b1d9a81bad
7
+ data.tar.gz: 769532a2d3f68fe97a20a001d73fc930855dbdd7c1dbe4ac115c29e2f7661f509c3d89cbe21480a7630e503af7bb3bea7b4fedbb625fb2e51e5a08aa4a1a1dc4
@@ -0,0 +1,2 @@
1
+ require 'ssrf_filter/ssrf_filter'
2
+ require 'ssrf_filter/version'
@@ -0,0 +1,214 @@
1
+ require 'ipaddr'
2
+ require 'net/http'
3
+ require 'resolv'
4
+ require 'uri'
5
+
6
+ class SsrfFilter
7
+ # https://en.wikipedia.org/wiki/Reserved_IP_addresses
8
+ IPV4_BLACKLIST = [
9
+ ::IPAddr.new('0.0.0.0/8'), # Current network (only valid as source address)
10
+ ::IPAddr.new('10.0.0.0/8'), # Private network
11
+ ::IPAddr.new('100.64.0.0/10'), # Shared Address Space
12
+ ::IPAddr.new('127.0.0.0/8'), # Loopback
13
+ ::IPAddr.new('169.254.0.0/16'), # Link-local
14
+ ::IPAddr.new('172.16.0.0/12'), # Private network
15
+ ::IPAddr.new('192.0.0.0/24'), # IETF Protocol Assignments
16
+ ::IPAddr.new('192.0.2.0/24'), # TEST-NET-1, documentation and examples
17
+ ::IPAddr.new('192.88.99.0/24'), # IPv6 to IPv4 relay (includes 2002::/16)
18
+ ::IPAddr.new('192.168.0.0/16'), # Private network
19
+ ::IPAddr.new('198.18.0.0/15'), # Network benchmark tests
20
+ ::IPAddr.new('198.51.100.0/24'), # TEST-NET-2, documentation and examples
21
+ ::IPAddr.new('203.0.113.0/24'), # TEST-NET-3, documentation and examples
22
+ ::IPAddr.new('224.0.0.0/4'), # IP multicast (former Class D network)
23
+ ::IPAddr.new('240.0.0.0/4'), # Reserved (former Class E network)
24
+ ::IPAddr.new('255.255.255.255') # Broadcast
25
+ ].freeze
26
+
27
+ IPV6_BLACKLIST = [
28
+ ::IPAddr.new('::1/128'), # Loopback
29
+ ::IPAddr.new('64:ff9b::/96'), # IPv4/IPv6 translation (RFC 6052)
30
+ ::IPAddr.new('100::/64'), # Discard prefix (RFC 6666)
31
+ ::IPAddr.new('2001::/32'), # Teredo tunneling
32
+ ::IPAddr.new('2001:10::/28'), # Deprecated (previously ORCHID)
33
+ ::IPAddr.new('2001:20::/28'), # ORCHIDv2
34
+ ::IPAddr.new('2001:db8::/32'), # Addresses used in documentation and example source code
35
+ ::IPAddr.new('2002::/16'), # 6to4
36
+ ::IPAddr.new('fc00::/7'), # Unique local address
37
+ ::IPAddr.new('fe80::/10'), # Link-local address
38
+ ::IPAddr.new('ff00::/8'), # Multicast
39
+ ].freeze
40
+
41
+ DEFAULT_SCHEME_WHITELIST = %w[http https].freeze
42
+
43
+ DEFAULT_RESOLVER = proc do |hostname|
44
+ ::Resolv.getaddresses(hostname).map { |ip| ::IPAddr.new(ip) }
45
+ end
46
+
47
+ DEFAULT_MAX_REDIRECTS = 10
48
+
49
+ VERB_MAP = {
50
+ get: ::Net::HTTP::Get,
51
+ put: ::Net::HTTP::Put,
52
+ post: ::Net::HTTP::Post,
53
+ delete: ::Net::HTTP::Delete
54
+ }.freeze
55
+
56
+ FIBER_LOCAL_KEY = :__ssrf_filter_hostname
57
+
58
+ class Error < ::StandardError
59
+ end
60
+
61
+ class InvalidUriScheme < Error
62
+ end
63
+
64
+ class PrivateIPAddress < Error
65
+ end
66
+
67
+ class UnresolvedHostname < Error
68
+ end
69
+
70
+ class TooManyRedirects < Error
71
+ end
72
+
73
+ %i[get put post delete].each do |method|
74
+ define_singleton_method(method) do |url, options = {}, &block|
75
+ original_url = url
76
+ scheme_whitelist = options[:scheme_whitelist] || DEFAULT_SCHEME_WHITELIST
77
+ resolver = options[:resolver] || DEFAULT_RESOLVER
78
+ max_redirects = options[:max_redirects] || DEFAULT_MAX_REDIRECTS
79
+ url = url.to_s
80
+
81
+ (max_redirects + 1).times do
82
+ uri = URI(url)
83
+
84
+ unless scheme_whitelist.include?(uri.scheme)
85
+ raise InvalidUriScheme, "URI scheme '#{uri.scheme}' not in whitelist: #{scheme_whitelist}"
86
+ end
87
+
88
+ hostname = uri.hostname
89
+ ip_addresses = resolver.call(hostname)
90
+ raise UnresolvedHostname, "Could not resolve hostname '#{hostname}'" if ip_addresses.empty?
91
+
92
+ public_addresses = ip_addresses.reject(&method(:unsafe_ip_address?))
93
+ raise PrivateIPAddress, "Hostname '#{hostname}' has no public ip addresses" if public_addresses.empty?
94
+
95
+ response = fetch_once(uri, public_addresses.sample.to_s, method, options, &block)
96
+
97
+ case response
98
+ when ::Net::HTTPRedirection then
99
+ url = response['location']
100
+ else
101
+ return response
102
+ end
103
+ end
104
+
105
+ raise TooManyRedirects, "Got #{max_redirects} redirects fetching #{original_url}"
106
+ end
107
+ end
108
+
109
+ def self.unsafe_ip_address?(ip_address)
110
+ return true if ipaddr_has_mask?(ip_address)
111
+
112
+ return IPV4_BLACKLIST.any? { |range| range.include?(ip_address) } if ip_address.ipv4?
113
+
114
+ if ip_address.ipv6?
115
+ result = IPV6_BLACKLIST.any? { |range| range.include?(ip_address) }
116
+ # TODO: convert these to be members of IPV6_BLACKLIST
117
+ result ||= ip_address.ipv4_compat? && IPV4_BLACKLIST.any? { |range| range.include?(ip_address.ipv4_compat) }
118
+ result ||= ip_address.ipv4_mapped? && IPV4_BLACKLIST.any? { |range| range.include?(ip_address.ipv4_mapped) }
119
+ return result
120
+ end
121
+
122
+ true
123
+ end
124
+ private_class_method :unsafe_ip_address?
125
+
126
+ def self.ipaddr_has_mask?(ipaddr)
127
+ range = ipaddr.to_range
128
+ range.first != range.last
129
+ end
130
+ private_class_method :ipaddr_has_mask?
131
+
132
+ def self.fetch_once(uri, ip, verb, options, &block)
133
+ if options[:params]
134
+ params = uri.query ? ::Hash[::URI.decode_www_form(uri.query)] : {}
135
+ params.merge!(options[:params])
136
+ uri.query = ::URI.encode_www_form(params)
137
+ end
138
+
139
+ hostname = uri.hostname
140
+ uri.hostname = ip
141
+
142
+ request = VERB_MAP[verb].new(uri)
143
+ request['host'] = hostname
144
+
145
+ Array(options[:headers]).each do |header, value|
146
+ request[header] = value
147
+ end
148
+
149
+ request.body = options[:body] if options[:body]
150
+
151
+ block.call(request) if block_given?
152
+
153
+ use_ssl = uri.scheme == 'https'
154
+ with_forced_hostname(hostname) do
155
+ ::Net::HTTP.start(uri.hostname, uri.port, use_ssl: use_ssl) do |http|
156
+ http.request(request)
157
+ end
158
+ end
159
+ end
160
+ private_class_method :fetch_once
161
+
162
+ def self.patch_ssl_socket!
163
+ return if instance_variable_defined?(:@patched_ssl_socket)
164
+
165
+ # What we'd like to do is have the following workflow:
166
+ # 1) resolve the hostname www.example.com, and choose a public ip address to connect to
167
+ # 2) connect to that specific ip address, to prevent things like DNS TOCTTOU bugs or other trickery
168
+ #
169
+ # Ideally this would happen by the ruby http library giving us control over DNS resolution,
170
+ # but it doesn't. Instead, when making the request we set the uri.hostname to the chosen ip address,
171
+ # and send a 'Host' header of the original hostname, i.e. connect to 'http://93.184.216.34/' and send
172
+ # a 'Host: www.example.com' header.
173
+ #
174
+ # This works for the http case, http://www.example.com. For the https case, this causes certificate
175
+ # validation failures, since the server certificate does not have a Subject Alternate Name for 93.184.216.34.
176
+ #
177
+ # Thus we perform the monkey-patch below, modifying SSLSocket's `post_connection_check(hostname)`
178
+ # and `hostname=(hostname)` methods:
179
+ # If our fiber local variable is set, use that for the hostname instead, otherwise behave as usual.
180
+ # The only time the variable will be set is if you are executing inside a `with_forced_hostname` block,
181
+ # which is used above.
182
+ #
183
+ # An alternative approach could be to pass in our own OpenSSL::X509::Store with a custom
184
+ # `verify_callback` to the ::Net::HTTP.start call, but this would require reimplementing certification
185
+ # validation, which is dangerous. This way we can piggyback on the existing validation and simply pretend
186
+ # that we connected to the desired hostname.
187
+
188
+ ::OpenSSL::SSL::SSLSocket.class_eval do
189
+ original_post_connection_check = instance_method(:post_connection_check)
190
+ define_method(:post_connection_check) do |hostname|
191
+ original_post_connection_check.bind(self).call(::Thread.current[FIBER_LOCAL_KEY] || hostname)
192
+ end
193
+
194
+ if method_defined?(:hostname=)
195
+ original_hostname = instance_method(:hostname=)
196
+ define_method(:hostname=) do |hostname|
197
+ original_hostname.bind(self).call(::Thread.current[FIBER_LOCAL_KEY] || hostname)
198
+ end
199
+ end
200
+ end
201
+
202
+ @patched_ssl_socket = true
203
+ end
204
+ private_class_method :patch_ssl_socket!
205
+
206
+ def self.with_forced_hostname(hostname, &_block)
207
+ patch_ssl_socket!
208
+ ::Thread.current[FIBER_LOCAL_KEY] = hostname
209
+ yield
210
+ ensure
211
+ ::Thread.current[FIBER_LOCAL_KEY] = nil
212
+ end
213
+ private_class_method :with_forced_hostname
214
+ end
@@ -0,0 +1,3 @@
1
+ class SsrfFilter
2
+ VERSION = '1.0.0'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ssrf_filter
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Arkadiy Tetelman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-07-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler-audit
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: coveralls
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.8'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.8'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.6'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.49'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.49'
69
+ - !ruby/object:Gem::Dependency
70
+ name: webmock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ description: A gem that makes it easy to prevent server side request forgery (SSRF)
84
+ attacks
85
+ email:
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - lib/ssrf_filter.rb
91
+ - lib/ssrf_filter/ssrf_filter.rb
92
+ - lib/ssrf_filter/version.rb
93
+ homepage: https://github.com/arkadiyt/ssrf_filter
94
+ licenses:
95
+ - MIT
96
+ metadata: {}
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: 2.0.0
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project:
113
+ rubygems_version: 2.4.6
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: A gem that makes it easy to prevent server side request forgery (SSRF) attacks
117
+ test_files: []