From 56e1b63333edf900ac4eb2f6d399f820d916a141 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 7 Sep 2026 16:57:56 +0900 Subject: [PATCH] Reject Bundler redirects that downgrade https to http The redirect handling only compared hosts, so an https source redirecting to http on the same host had the request's user and password copied onto a plaintext connection. Gem::RemoteFetcher already refuses such a redirect. Co-Authored-By: Claude Opus 5 --- lib/bundler/fetcher/downloader.rb | 9 +++++++++ spec/bundler/fetcher/downloader_spec.rb | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/bundler/fetcher/downloader.rb b/lib/bundler/fetcher/downloader.rb index 59e54c819de0..8050c0a3f242 100644 --- a/lib/bundler/fetcher/downloader.rb +++ b/lib/bundler/fetcher/downloader.rb @@ -51,6 +51,11 @@ def fetch(uri, headers = {}, counter = 0) response when Gem::Net::HTTPRedirection new_uri = Gem::URI.parse(response["location"]) + # Following a downgrade would put the credentials this request carries + # on a plaintext connection, so refuse it like Gem::RemoteFetcher does. + if https?(uri) && !https?(new_uri) + raise HTTPError, "Redirecting to a non-https URI is not allowed: #{URICredentialsFilter.credential_filtered_uri(new_uri)}" + end if new_uri.host == uri.host new_uri.user = uri.user new_uri.password = uri.password @@ -120,6 +125,10 @@ def network_down_error(uri, filtered_uri) "connection and try again.") end + def https?(uri) + uri.scheme&.casecmp("https")&.zero? + end + def validate_uri_scheme!(uri) return if /\Ahttps?\z/.match?(uri.scheme) raise InvalidOption, diff --git a/spec/bundler/fetcher/downloader_spec.rb b/spec/bundler/fetcher/downloader_spec.rb index a3390ac5276f..a7f642f456e6 100644 --- a/spec/bundler/fetcher/downloader_spec.rb +++ b/spec/bundler/fetcher/downloader_spec.rb @@ -59,6 +59,19 @@ subject.fetch(uri, options, counter) end end + + context "when the redirect uri downgrades https to http" do + let(:uri) { Gem::URI("https://username:password@www.uri-to-fetch.com/api/v2/endpoint") } + + before { http_response["location"] = "http://www.uri-to-fetch.com/api/v2/endpoint" } + + it "should raise a Bundler::HTTPError instead of following the redirect" do + expect(subject).to receive(:fetch).with(uri, options, 0).and_call_original + expect(subject).not_to receive(:fetch).with(Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint"), options, 1) + expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::HTTPError, + "Redirecting to a non-https URI is not allowed: http://www.uri-to-fetch.com/api/v2/endpoint") + end + end end context "when the request response is a Gem::Net::HTTPSuccess" do