Package: release.debian.org Control: affects -1 + src:ruby-oauth2 X-Debbugs-Cc: [email protected] User: [email protected] Usertags: pu Tags: trixie Severity: normal
[ Reason ] ruby-oauth2 in Trixie is 2.0.9-1, and it's hit by CVE-2026-54603 (GHSA-pp92-crg2-gfv9). Client#request follows Location with URI#merge. If that Location is protocol-relative, the //other-host/... form, the host gets replaced and the Authorization header goes with the request, so the bearer token ends up at whatever host the redirect named. An absolute URL on another origin keeps the header too. The tracker has this as no-dsa, minor, on Trixie. Unstable is already fixed in 2.0.25-1, which is upstream 2.0.22, commit 0f0a474. #1143054 is closed there, so this upload doesn't close it again. [ Impact ] If a caller is sending a bearer token with this client, a server that answers with Location: //other-host/... or with an absolute URL on another origin gets that token forwarded. [ Tests ] I turned the upstream rspec suite on. debian/ruby-tests.rake.disabled is now debian/ruby-tests.rake, and it runs spec/**/*_spec.rb. The examples all run. gem2deb-test-runner in the Trixie build was 462 examples, 0 failures. The redirect coverage is the three examples from commit 0f0a474, patched into spec/oauth2/client_spec.rb. A protocol-relative Location stays on the original host and keeps Authorization. An absolute cross-origin redirect is still followed, but Authorization comes off. A cross-origin redirect that never had that header still follows. One of the older examples was looking for a particular XML error string. Trixie's REXML raises MultiXml::ParseError with a different message, and upstream 2.0.25 only checks the class, so I did the same. That patch doesn't touch the library. .rspec is still --order random, which is #858254, and I'm leaving that bug alone. I also tried a local script against both packages. On the archive package the bearer token was sent to the host named in Location, for the protocol-relative redirect and for the absolute one. With this package the protocol-relative request stayed on the original host and kept the token, the absolute redirect was followed with the token removed, and a normal AccessToken#get and client.request still returned the body I expected. [ Risks ] This is the 2.0.22 fix, fitted onto 2.0.9's client.rb. Three private methods. A normal token request, and a same-origin relative redirect, still behave as they do in 2.0.9-1. A protocol-relative Location stays on the original origin. A redirect that changes scheme, host, or port is still followed, with Authorization removed and the other headers kept. 2.0.23 rewrote that protocol-relative handling for Ruby 2.4, which Trixie doesn't need. Trixie is Ruby 3.3.8, and the 2.0.22 form already stays on the original host there. Sid's 2.0.25 does the same thing. [ Checklist ] [x] *all* changes are documented in the d/changelog [x] I reviewed all changes and I approve them [x] attach debdiff against the package in (old)stable [x] the issue is verified as fixed in unstable [ Changes ] The debdiff against Trixie 2.0.9-1 is debian/changelog, debian/control, debian/patches/series, CVE-2026-54603-protocol-relative-redirect.patch, accept-rexml-parse-error.patch, debian/ruby-tests.rake, and the removal of debian/ruby-tests.rake.disabled. The CVE patch is the client.rb backport plus the three specs from 0f0a474, with the quotes adjusted for 2.0.9. The other patch is only that XML assertion. Build-Depends picks up ruby-backports, ruby-rspec-block-is-expected, ruby-rspec-pending-for, ruby-rspec-stubbed-env, and ruby-silent-stream, all of which are already in Trixie. Version is 2.0.9-1+deb13u1, urgency=medium, distribution trixie. Best regards, Simon Quigley [email protected]
diff --git a/debian/changelog b/debian/changelog index 4126c08..77c3a73 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,12 @@ +ruby-oauth2 (2.0.9-1+deb13u1) trixie; urgency=medium + + * CVE-2026-54603: keep a protocol-relative redirect on the same + origin, and drop Authorization when a redirect changes origin. + * Accept REXML's MultiXml::ParseError in the XML parse example. + * Run the upstream test suite. + + -- Simon Quigley <[email protected]> Thu, 01 Oct 2026 21:55:55 -0500 + ruby-oauth2 (2.0.9-1) unstable; urgency=medium * Team upload diff --git a/debian/control b/debian/control index 8b3e752..c819cf3 100644 --- a/debian/control +++ b/debian/control @@ -7,12 +7,17 @@ Build-Depends: debhelper-compat (= 13), gem2deb, rake, ruby-addressable, + ruby-backports, ruby-faraday, ruby-jwt, ruby-multi-json, ruby-multi-xml, ruby-rack, ruby-rspec, + ruby-rspec-block-is-expected, + ruby-rspec-pending-for, + ruby-rspec-stubbed-env, + ruby-silent-stream, ruby-simplecov, ruby-coveralls, ruby-rash-alt, diff --git a/debian/patches/CVE-2026-54603-protocol-relative-redirect.patch b/debian/patches/CVE-2026-54603-protocol-relative-redirect.patch new file mode 100644 index 0000000..4a77f79 --- /dev/null +++ b/debian/patches/CVE-2026-54603-protocol-relative-redirect.patch @@ -0,0 +1,112 @@ +Description: Keep protocol-relative redirects on the same origin + OAuth2::Client#request followed Location with URI#merge. A + protocol-relative Location (//host/...) replaced the request authority, so + the Authorization header was sent to that host. Resolve those locations as + a same-origin path, and drop Authorization when a redirect changes scheme, + host, or port. +Origin: upstream, https://github.com/ruby-oauth/oauth2/commit/0f0a474f1b38453e119e660c2daca742d4378ce9 +Bug: https://github.com/ruby-oauth/oauth2/security/advisories/GHSA-pp92-crg2-gfv9 +Bug-Debian: https://bugs.debian.org/1143054 +Author: Peter H. Boling <[email protected]> +Forwarded: not-needed +Last-Update: 2026-10-01 + +--- a/lib/oauth2/client.rb ++++ b/lib/oauth2/client.rb +@@ -125,8 +125,9 @@ module OAuth2 + end + location = response.headers['location'] + if location +- full_location = response.response.env.url.merge(location) +- request(verb, full_location, opts) ++ current_location = response.response.env.url ++ full_location = resolve_redirect_location(current_location, location) ++ request(verb, full_location, sanitize_redirect_options(opts, current_location, full_location)) + else + error = Error.new(response) + raise(error, "Got #{response.status} status code, but no Location header was present") +@@ -295,6 +296,36 @@ module OAuth2 + Response.new(response, parse: parse, snaky: snaky) + end + ++ def resolve_redirect_location(current_location, location) ++ safe_location = ++ if location.respond_to?(:start_with?) && location.start_with?('//') ++ "./#{location}" ++ else ++ location ++ end ++ ++ current_location.merge(safe_location) ++ end ++ ++ def sanitize_redirect_options(req_opts, current_location, next_location) ++ return req_opts unless cross_origin_redirect?(current_location, next_location) ++ ++ headers = req_opts[:headers] ++ return req_opts unless headers && headers.any? { |key, _value| key.to_s.casecmp('Authorization').zero? } ++ ++ safe_opts = req_opts.dup ++ safe_headers = headers.dup ++ safe_headers.delete_if { |key, _value| key.to_s.casecmp('Authorization').zero? } ++ safe_opts[:headers] = safe_headers ++ safe_opts ++ end ++ ++ def cross_origin_redirect?(current_location, next_location) ++ current_location.scheme != next_location.scheme || ++ current_location.host != next_location.host || ++ current_location.port != next_location.port ++ end ++ + # Returns the authenticator object + # + # @return [Authenticator] the initialized Authenticator +--- a/spec/oauth2/client_spec.rb ++++ b/spec/oauth2/client_spec.rb +@@ -13,6 +13,10 @@ RSpec.describe OAuth2::Client do + stub.get('/unauthorized') { |_env| [401, {'Content-Type' => 'application/json'}, JSON.dump(error: error_value, error_description: error_description_value)] } + stub.get('/conflict') { |_env| [409, {'Content-Type' => 'text/plain'}, 'not authorized'] } + stub.get('/redirect') { |_env| [302, {'Content-Type' => 'text/plain', 'location' => '/success'}, ''] } ++ stub.get('/protocol_relative_redirect') { |_env| [302, {'Content-Type' => 'text/plain', 'location' => '//attacker.example/leak'}, ''] } ++ stub.get('///attacker.example/leak') { |env| [200, {}, JSON.dump(auth: env[:request_headers]['Authorization'], url: env[:url].to_s)] } ++ stub.get('/absolute_cross_origin_redirect') { |_env| [302, {'Content-Type' => 'text/plain', 'location' => 'https://attacker.example/leak'}, ''] } ++ stub.get('/leak') { |env| [200, {}, JSON.dump(auth: env[:request_headers]['Authorization'], marker: env[:request_headers]['X-Marker'], url: env[:url].to_s)] } + stub.get('/redirect_no_loc') { |_env| [302, {'Content-Type' => 'text/plain'}, ''] } + stub.post('/redirect') { |_env| [303, {'Content-Type' => 'text/plain', 'location' => '/reflect'}, ''] } + stub.get('/error') { |_env| [500, {'Content-Type' => 'text/plain'}, 'unknown error'] } +@@ -364,6 +368,34 @@ RSpec.describe OAuth2::Client do + expect(response.response.env.url.to_s).to eq('https://api.example.com/success') + end + ++ it 'treats protocol-relative redirect locations as same-origin paths' do ++ response = subject.request(:get, '/protocol_relative_redirect', headers: {'Authorization' => 'Bearer secret'}) ++ body = JSON.parse(response.body) ++ ++ expect(body['auth']).to eq('Bearer secret') ++ expect(body['url']).to eq('https://api.example.com///attacker.example/leak') ++ expect(response.response.env.url.to_s).to eq('https://api.example.com///attacker.example/leak') ++ end ++ ++ it 'removes Authorization headers from cross-origin redirects' do ++ response = subject.request(:get, '/absolute_cross_origin_redirect', headers: {'Authorization' => 'Bearer secret', 'X-Marker' => 'kept'}) ++ body = JSON.parse(response.body) ++ ++ expect(body['auth']).to be_nil ++ expect(body['marker']).to eq('kept') ++ expect(body['url']).to eq('https://attacker.example/leak') ++ expect(response.response.env.url.to_s).to eq('https://attacker.example/leak') ++ end ++ ++ it 'follows cross-origin redirects when there are no credential headers to remove' do ++ response = subject.request(:get, '/absolute_cross_origin_redirect') ++ body = JSON.parse(response.body) ++ ++ expect(body['auth']).to be_nil ++ expect(body['url']).to eq('https://attacker.example/leak') ++ expect(response.response.env.url.to_s).to eq('https://attacker.example/leak') ++ end ++ + it 'redirects using GET on a 303' do + response = subject.request(:post, '/redirect', body: 'foo=bar') + expect(response.body).to be_empty diff --git a/debian/patches/accept-rexml-parse-error.patch b/debian/patches/accept-rexml-parse-error.patch new file mode 100644 index 0000000..034d04d --- /dev/null +++ b/debian/patches/accept-rexml-parse-error.patch @@ -0,0 +1,22 @@ +Description: Match MultiXml::ParseError by class + The XML example asserts the parser's old error string. Trixie's REXML + raises MultiXml::ParseError with a different message. Upstream 2.0.25 + checks only the exception class. +Origin: upstream, https://github.com/ruby-oauth/oauth2/blob/v2.0.25/spec/oauth2/client_spec.rb +Forwarded: not-needed +Last-Update: 2026-10-01 + +--- a/spec/oauth2/client_spec.rb ++++ b/spec/oauth2/client_spec.rb +@@ -530,10 +530,7 @@ RSpec.describe OAuth2::Client do + end + end + +- expect { client.get_token(parse: :xml) }.to raise_error( +- MultiXml::ParseError, +- 'The document "{\"access_token\":\"the-token\"}" does not have a valid root' +- ) ++ expect { client.get_token(parse: :xml) }.to raise_error(MultiXml::ParseError) + end + end + diff --git a/debian/patches/series b/debian/patches/series new file mode 100644 index 0000000..93f2de5 --- /dev/null +++ b/debian/patches/series @@ -0,0 +1,2 @@ +CVE-2026-54603-protocol-relative-redirect.patch +accept-rexml-parse-error.patch diff --git a/debian/ruby-tests.rake b/debian/ruby-tests.rake new file mode 100644 index 0000000..6586b64 --- /dev/null +++ b/debian/ruby-tests.rake @@ -0,0 +1,11 @@ +require 'rspec/core/rake_task' + +# spec/spec_helper.rb loads codecov and extra SimpleCov formatters unless CI +# is set. Those libraries are not in trixie. The specs themselves still run. +ENV['CI'] = 'true' + +RSpec::Core::RakeTask.new(:spec) do |spec| + spec.pattern = './spec/**/*_spec.rb' +end + +task default: :spec diff --git a/debian/ruby-tests.rake.disabled b/debian/ruby-tests.rake.disabled deleted file mode 100644 index 89a753d..0000000 --- a/debian/ruby-tests.rake.disabled +++ /dev/null @@ -1,7 +0,0 @@ -require 'rspec/core/rake_task' - -RSpec::Core::RakeTask.new(:spec) do |spec| - spec.pattern = './spec/**/*_spec.rb' -end - -task :default => :spec
OpenPGP_signature.asc
Description: OpenPGP digital signature

