Protocol-relative redirect Location overrides authority in OAuth2::Client#request, leaking bearer Authorization to attacker host
Published: June 07, 2026
SECURITY IDENTIFIERS
- GHSA: GHSA-pp92-crg2-gfv9
- Vendor Advisory: https://github.com/ruby-oauth/oauth2/security/advisories/GHSA-pp92-crg2-gfv9
GEM
SEVERITY
CVSS v3.x: 8.6 (High)
UNAFFECTED VERSIONS
< 0.4.0
PATCHED VERSIONS
>= 2.0.22
DESCRIPTION
Summary
When an application uses OAuth2::Client (typically via an OAuth2::AccessToken) and the configured authorization server returns a redirect whose Location header is a protocol-relative URI of the form //attacker.example/leak, OAuth2::Client#request resolves the redirect with response.response.env.url.merge(location). Per RFC 3986 §5.2, an input that starts with // is a network-path reference and replaces the authority of the base URL: URI("http://idp.trusted/userinfo").merge("//attacker.example/leak") returns http://attacker.example/leak. The recursive request(verb, full_location, req_opts) call then re-sends the request to the attacker host while preserving the Authorization: Bearer <access-token> header that OAuth2::AccessToken#configure_authentication! installed on req_opts[:headers] for the original request.
The result is a one-shot cross-origin credential disclosure: any 30x response from the IdP that an attacker can influence (a compromised endpoint, a tenant-controlled IdP in a multi-tenant deployment, or an open-redirect handler that does not normalise the Location it emits) can extract the bearer access token of the calling user.
Affected
- oauth2 v2.0.21 and all prior versions back to and including v0.4.0.
- The underlying unsafe redirect-following behavior that reuses headers starts in v0.4.0 via b944da5.
- The vulnerability was retained when the code was refactored to a redirect helper, and in this form has been present in OAuth2::Client#request since v2.0.0 via b944da5.
- Ruby 4.0.5 on arm64-darwin25. The behaviour of URI#merge for protocol-relative inputs is RFC-conformant and the same on every supported Ruby (≥ 2.x).
- Adapter independence: confirmed against the default faraday 2.14.2 + faraday-net_http 3.4.3 stack. The URI#merge call is in oauth2 itself, not in Faraday, so the issue is not adapter-specific.
Impact
A consumer that uses OAuth2::AccessToken#get / #post / #request against an IdP whose redirect target an attacker can influence (open redirect, malicious tenant, or in-path adversary) loses two things at once:
- Cross-origin credential disclosure. The connection-scoped Authorization: Bearer <token> header attached by OAuth2::AccessToken#configure_authentication! is sent to the attacker host on the very next request, with no second user interaction.
- SSRF from the application server. The OAuth2 client follows the redirect on behalf of the application, so the host that ultimately receives the request is one the attacker chooses — useful for hitting internal addresses (//169.254.169.254/…, //127.0.0.1:…/…) that the application server can reach but the attacker cannot.
The combined primitive is stronger than the usual cross-origin-redirect leak because no application-level cooperation is required and no Location: http://attacker/… is needed — the protocol-relative //attacker/x form slips past naive scheme-based Location filters that allow same-scheme-implicit redirects.
Credit
- Reported by tonghuaroot.
