Skip to content

Commit

Permalink
Add option to allow unfollowed redirects (#63)
Browse files Browse the repository at this point in the history
* Resolve conflicts

* Some updates

* Update documentation

---------

Co-authored-by: Arkadiy Tetelman <[email protected]>
  • Loading branch information
elliterate and arkadiyt committed Sep 1, 2023
1 parent 49e2997 commit 14e340a
Show file tree
Hide file tree
Showing 3 changed files with 26 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
15 changes: 10 additions & 5 deletions lib/ssrf_filter/ssrf_filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions spec/lib/ssrf_filter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'})
Expand Down

0 comments on commit 14e340a

Please sign in to comment.