FIX: `FinalDestination#get` forwarding `Authorization` header on redirects (#27043)

This commits updates `FinalDestination#get` to not forward
`Authorization` header on redirects since most HTTP clients I tested like
curl and wget does not it.

This also fixes a recent problem in `DiscourseIpInfo.mmdb_download`
where we will fail to download the databases when both `GlobalSetting.maxmind_account_id` and
`GlobalSetting.maxmind_license_key` has been set. The failure is due to
the bug above where the redirected URL given by MaxMind does not accept
an `Authorization` header.
This commit is contained in:
Alan Guo Xiang Tan 2024-05-16 08:37:34 +08:00 committed by GitHub
parent 33871c4830
commit e31cf66f11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 47 additions and 11 deletions

View File

@ -9,7 +9,19 @@ RSpec.describe DiscourseIpInfo do
stub_request( stub_request(
:get, :get,
"https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz", "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz",
).with(basic_auth: %w[account_id license_key]).to_return(status: 200, body: "", headers: {}) ).with(basic_auth: %w[account_id license_key]).to_return(
status: 302,
body: "",
headers: {
location:
"https://mm-prod-geoip-databases.a2649acb697e2c09b632799562c076f2.r2.cloudflarestorage.com/some-path",
},
)
stub_request(
:get,
"https://mm-prod-geoip-databases.a2649acb697e2c09b632799562c076f2.r2.cloudflarestorage.com/some-path",
).with { |req| expect(req.headers.key?("Authorization")).to eq(false) }.to_return(status: 200)
described_class.mmdb_download("GeoLite2-City") described_class.mmdb_download("GeoLite2-City")
end end

View File

@ -156,7 +156,7 @@ class FinalDestination
# this is a new interface for simply getting # this is a new interface for simply getting
# N bytes accounting for all internal logic # N bytes accounting for all internal logic
def get(redirects = @limit, extra_headers: {}, &blk) def get(redirects = @limit, extra_headers: {}, except_headers: [], &blk)
raise "Must specify block" unless block_given? raise "Must specify block" unless block_given?
if @uri && @uri.port == 80 && FinalDestination.is_https_domain?(@uri.hostname) if @uri && @uri.port == 80 && FinalDestination.is_https_domain?(@uri.hostname)
@ -167,7 +167,7 @@ class FinalDestination
return if !validate_uri return if !validate_uri
return if @stop_at_blocked_pages && blocked_domain?(@uri) return if @stop_at_blocked_pages && blocked_domain?(@uri)
result, headers_subset = safe_get(@uri, &blk) result, headers_subset = safe_get(@uri, except_headers:, &blk)
return if !result return if !result
cookie = headers_subset.set_cookie cookie = headers_subset.set_cookie
@ -198,7 +198,12 @@ class FinalDestination
extra = nil extra = nil
extra = { "Cookie" => cookie } if cookie extra = { "Cookie" => cookie } if cookie
get(redirects - 1, extra_headers: extra, &blk) # Most HTTP Clients strip the Authorization header on redirects as the client could be redirecting to a untrusted
# party. Not stripping the Authorization header on redirect can also lead to problems where the
# redirected party does not expect a Authorization header and thus rejects the request.
except_headers = ["Authorization"]
get(redirects - 1, extra_headers: extra, except_headers:, &blk)
elsif result == :ok elsif result == :ok
@uri.to_s @uri.to_s
else else
@ -478,7 +483,7 @@ class FinalDestination
protected protected
def safe_get(uri) def safe_get(uri, except_headers: [])
result = nil result = nil
unsafe_close = false unsafe_close = false
headers_subset = Struct.new(:location, :set_cookie).new headers_subset = Struct.new(:location, :set_cookie).new
@ -488,7 +493,7 @@ class FinalDestination
request_headers.merge( request_headers.merge(
"Accept-Encoding" => "gzip", "Accept-Encoding" => "gzip",
"Host" => uri.hostname + (@include_port_in_host_header ? ":#{uri.port}" : ""), "Host" => uri.hostname + (@include_port_in_host_header ? ":#{uri.port}" : ""),
) ).except(*except_headers)
req = FinalDestination::HTTP::Get.new(uri.request_uri, headers) req = FinalDestination::HTTP::Get.new(uri.request_uri, headers)

View File

@ -460,7 +460,9 @@ RSpec.describe FinalDestination do
before { described_class.clear_https_cache!("wikipedia.com") } before { described_class.clear_https_cache!("wikipedia.com") }
context "when there is a redirect" do context "when there is a redirect" do
before do after { WebMock.reset! }
it "correctly streams" do
stub_request(:get, "http://wikipedia.com/").to_return( stub_request(:get, "http://wikipedia.com/").to_return(
status: 302, status: 302,
body: "", body: "",
@ -468,6 +470,7 @@ RSpec.describe FinalDestination do
"location" => "https://wikipedia.com/", "location" => "https://wikipedia.com/",
}, },
) )
# webmock does not do chunks # webmock does not do chunks
stub_request(:get, "https://wikipedia.com/").to_return( stub_request(:get, "https://wikipedia.com/").to_return(
status: 200, status: 200,
@ -475,11 +478,7 @@ RSpec.describe FinalDestination do
headers: { headers: {
}, },
) )
end
after { WebMock.reset! }
it "correctly streams" do
chunk = nil chunk = nil
result = result =
fd.get do |resp, c| fd.get do |resp, c|
@ -490,6 +489,26 @@ RSpec.describe FinalDestination do
expect(result).to eq("https://wikipedia.com/") expect(result).to eq("https://wikipedia.com/")
expect(chunk).to eq("<html><head>") expect(chunk).to eq("<html><head>")
end end
it "does not forward 'Authorization' header to subsequent hosts" do
fd =
FinalDestination.new(
"http://wikipedia.com",
headers: {
"Authorization" => "Basic #{Base64.strict_encode64("account_id:license_key")}",
},
)
stub_request(:get, "http://wikipedia.com").with(
basic_auth: %w[account_id license_key],
).to_return(status: 302, body: "", headers: { "Location" => "http://some.host.com/" })
stub_request(:get, "http://some.host.com/")
.with { |req| expect(req.headers.key?("Authorization")).to eq(false) }
.to_return(status: 200, body: "")
fd.get {}
end
end end
context "when there is a timeout" do context "when there is a timeout" do