From 14e340ae7548ea4ca9f4b8f22f01ae68a63fcf1c Mon Sep 17 00:00:00 2001 From: Ian Lesperance Date: Fri, 1 Sep 2023 15:10:48 -0400 Subject: [PATCH] Add option to allow unfollowed redirects (#63) * Resolve conflicts * Some updates * Update documentation --------- Co-authored-by: Arkadiy Tetelman --- README.md | 1 + lib/ssrf_filter/ssrf_filter.rb | 15 ++++++++++----- spec/lib/ssrf_filter_spec.rb | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ee35a76..9c5a2c5 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Options hash: - `:body` — Body to send with the request. - `:http_options` – Options to pass to [Net::HTTP.start](https://ruby-doc.org/stdlib-2.6.4/libdoc/net/http/rdoc/Net/HTTP.html#method-c-start). Use this to set custom timeouts or SSL options. - `:request_proc` - a proc that receives the request object, for custom modifications before sending the request. +- `:allow_unfollowed_redirects` - If true and your request hits the maximum number of redirects, the last response will be returned instead of raising an error. Defaults to false. Returns: diff --git a/lib/ssrf_filter/ssrf_filter.rb b/lib/ssrf_filter/ssrf_filter.rb index c3bd73e..91b23ca 100644 --- a/lib/ssrf_filter/ssrf_filter.rb +++ b/lib/ssrf_filter/ssrf_filter.rb @@ -72,6 +72,7 @@ def self.prefixlen_from_ipaddr(ipaddr) ::Resolv.getaddresses(hostname).map { |ip| ::IPAddr.new(ip) } end + DEFAULT_ALLOW_UNFOLLOWED_REDIRECTS = false DEFAULT_MAX_REDIRECTS = 10 VERB_MAP = { @@ -108,11 +109,13 @@ class CRLFInjection < Error ::SsrfFilter::Patch::SSLSocket.apply! original_url = url - scheme_whitelist = options[:scheme_whitelist] || DEFAULT_SCHEME_WHITELIST - resolver = options[:resolver] || DEFAULT_RESOLVER - max_redirects = options[:max_redirects] || DEFAULT_MAX_REDIRECTS + scheme_whitelist = options.fetch(:scheme_whitelist, DEFAULT_SCHEME_WHITELIST) + resolver = options.fetch(:resolver, DEFAULT_RESOLVER) + allow_unfollowed_redirects = options.fetch(:allow_unfollowed_redirects, DEFAULT_ALLOW_UNFOLLOWED_REDIRECTS) + max_redirects = options.fetch(:max_redirects, DEFAULT_MAX_REDIRECTS) url = url.to_s + response = nil (max_redirects + 1).times do uri = URI(url) @@ -131,6 +134,8 @@ class CRLFInjection < Error return response if url.nil? end + return response if allow_unfollowed_redirects + raise TooManyRedirects, "Got #{max_redirects} redirects fetching #{original_url}" end end @@ -197,10 +202,10 @@ def self.fetch_once(uri, ip, verb, options, &block) url = response['location'] # Handle relative redirects url = "#{uri.scheme}://#{hostname}:#{uri.port}#{url}" if url.start_with?('/') - return nil, url else - return response, nil + url = nil end + return response, url end end end diff --git a/spec/lib/ssrf_filter_spec.rb b/spec/lib/ssrf_filter_spec.rb index 4e0a36b..897ae8f 100644 --- a/spec/lib/ssrf_filter_spec.rb +++ b/spec/lib/ssrf_filter_spec.rb @@ -460,6 +460,21 @@ def inject_custom_trust_store(*certificates) end.to raise_error(described_class::TooManyRedirects) end + it 'returns the last response if there are too many redirects and unfollowed redirects are allowed' do + stub_request(:get, "https://#{public_ipv4}").with(headers: {host: 'www.example.com'}) + .to_return(status: 301, headers: {location: 'https://www.example2.com'}) + resolver = proc { [public_ipv4] } + response = + described_class.get( + 'https://www.example.com', + resolver: resolver, + allow_unfollowed_redirects: true, + max_redirects: 0 + ) + expect(response.code).to eq('301') + expect(response['location']).to eq('https://www.example2.com') + end + it 'fails if the redirected url is not in the scheme whitelist' do stub_request(:put, "https://#{public_ipv4}").with(headers: {host: 'www.example.com'}) .to_return(status: 301, headers: {location: 'ftp://www.example.com'})