Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance security by removing Authorization header on HTTP redirects #36

Merged
merged 4 commits into from
Sep 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions lib/open-uri.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ module OpenURI
:redirect => true,
:encoding => nil,
:max_redirects => 64,
:request_specific_fields => nil,
}

def OpenURI.check_options(options) # :nodoc:
Expand Down Expand Up @@ -148,7 +149,11 @@ def OpenURI.open_uri(name, *rest) # :nodoc:
end
encoding = Encoding.find(options[:encoding])
end

if options.has_key? :request_specific_fields
if !(options[:request_specific_fields].is_a?(Hash) || options[:request_specific_fields].is_a?(Proc))
raise ArgumentError, "Invalid request_specific_fields option: #{options[:request_specific_fields].inspect}"
end
end
unless mode == nil ||
mode == 'r' || mode == 'rb' ||
mode == File::RDONLY
Expand Down Expand Up @@ -215,9 +220,17 @@ def OpenURI.open_loop(uri, options) # :nodoc:
max_redirects = options[:max_redirects] || Options.fetch(:max_redirects)
buf = nil
while true
request_specific_fields = {}
if options.has_key? :request_specific_fields
request_specific_fields = if options[:request_specific_fields].is_a?(Hash)
options[:request_specific_fields]
else options[:request_specific_fields].is_a?(Proc)
options[:request_specific_fields].call(uri)
end
end
redirect = catch(:open_uri_redirect) {
buf = Buffer.new
uri.buffer_open(buf, find_proxy.call(uri), options)
uri.buffer_open(buf, find_proxy.call(uri), options.merge(request_specific_fields))
nil
}
if redirect
Expand All @@ -237,6 +250,10 @@ def OpenURI.open_loop(uri, options) # :nodoc:
options = options.dup
options.delete :http_basic_authentication
end
if options.include?(:request_specific_fields) && options[:request_specific_fields].is_a?(Hash)
# Send request specific headers only for the initial request.
options.delete :request_specific_fields
end
uri = redirect
raise "HTTP redirection loop: #{uri}" if uri_set.include? uri.to_s
uri_set[uri.to_s] = true
Expand Down Expand Up @@ -752,6 +769,38 @@ module OpenRead
#
# Number of HTTP redirects allowed before OpenURI::TooManyRedirects is raised.
# The default is 64.
#
# [:request_specific_fields]
# Synopsis:
# :request_specific_fields => {}
# :request_specific_fields => lambda {|url| ...}
#
# :request_specific_fields option allows specifying custom header fields that
# are sent with the HTTP request. It can be passed as a Hash or a Proc that
# gets evaluated on each request and returns a Hash of header fields.
#
# If a Hash is provided, it specifies the headers only for the initial
# request and these headers will not be sent on redirects.
#
# If a Proc is provided, it will be executed for each request including
# redirects, allowing dynamic header customization based on the request URL.
# It is important that the Proc returns a Hash. And this Hash specifies the
# headers to be sent with the request.
#
# For Example with Hash
# URI.open("http://...",
# request_specific_fields: {"Authorization" => "token dummy"}) {|f| ... }
#
# For Example with Proc:
# URI.open("http://...",
# request_specific_fields: lambda { |uri|
# if uri.host == "example.com"
# {"Authorization" => "token dummy"}
# else
# {}
# end
# }) {|f| ... }
#
def open(*rest, &block)
OpenURI.open_uri(self, *rest, &block)
end
Expand Down
60 changes: 60 additions & 0 deletions test/open-uri/test_open-uri.rb
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,66 @@ def test_redirect_auth_failure_r1
}
end

def test_redirect_without_request_specific_fields_hash
authorization_header = nil
redirected_authorization_header = nil
with_http {|srv, url|
srv.mount_proc("/r1/", lambda {|req, res| res.status = 301; res["location"] = "#{url}/r2"; authorization_header = req["Authorization"]; } )
srv.mount_proc("/r2/", lambda {|req, res| redirected_authorization_header = req["Authorization"]; } )
URI.open("#{url}/r1/", "Authorization" => "dummy_token") {|f|
assert_equal("dummy_token", authorization_header)
assert_equal("#{url}/r2", f.base_uri.to_s)
assert_equal("dummy_token", redirected_authorization_header)
}
}
end

def test_redirect_with_request_specific_fields_hash
authorization_header = nil
redirected_authorization_header = "exposed_dummy_token"
with_http {|srv, url|
srv.mount_proc("/r1/", lambda {|req, res| res.status = 301; res["location"] = "#{url}/r2"; authorization_header = req["Authorization"]; } )
srv.mount_proc("/r2/", lambda {|req, res| redirected_authorization_header = req["Authorization"]; } )
URI.open("#{url}/r1/", request_specific_fields: {"Authorization" => "dummy_token"}) {|f|
assert_equal("dummy_token", authorization_header)
assert_equal("#{url}/r2", f.base_uri.to_s)
assert_equal(nil, redirected_authorization_header)
}
}
end

def test_redirect_with_request_specific_fields_proc
authorization_header = nil
redirected_authorization_header = nil

modify_authorization_header = Proc.new do |uri|
authorization_token = if uri.to_s.include?("/r1")
"dummy_token"
else
"masked_dummy_token"
end
{ "Authorization" => authorization_token }
end

with_http {|srv, url|
srv.mount_proc("/r1/", lambda {|req, res| res.status = 301; res["location"] = "#{url}/r2"; authorization_header = req["Authorization"] } )
srv.mount_proc("/r2/", lambda {|req, res| redirected_authorization_header = req["Authorization"]; } )
URI.open("#{url}/r1/", request_specific_fields: modify_authorization_header) {|f|
assert_equal("dummy_token", authorization_header)
assert_equal("#{url}/r2", f.base_uri.to_s)
assert_equal("masked_dummy_token", redirected_authorization_header)
}
}
end

def test_redirect_with_invalid_request_specific_fields_format
with_http {|srv, url|
srv.mount_proc("/r1/", lambda {|req, res| res.body = "r1" } )
exc = assert_raise(ArgumentError) { URI.open("#{url}/r1/", request_specific_fields: "dummy_token") {} }
assert_equal('Invalid request_specific_fields option: "dummy_token"', exc.message)
}
end

def test_max_redirects_success
with_http {|srv, url|
srv.mount_proc("/r1/", lambda {|req, res| res.status = 301; res["location"] = "#{url}/r2"; res.body = "r1" } )
Expand Down