Repository navigation
Conversation
AccessToken#request temporarily swaps consumer.uri to any absolute URI passed to #get/#post/#put/#patch/#delete/#head and sends the OAuth-signed request there. An application that forwards an untrusted/user-influenced absolute URL into these helpers would send the signed OAuth Authorization header to an arbitrary host (header disclosure) or to an internal address (SSRF). The signed header exposes oauth_consumer_key and oauth_token (the secrets stay the HMAC key and are never sent), but that is still a credential/identifier leak plus a request-forwarding primitive. This mirrors the existing token_request_cross_origin_redirects hardening: add an access_token_cross_origin_requests consumer option (default false). With it off, an absolute service URI whose origin (scheme, host, or effective port) differs from the configured :site raises OAuth::Error before any request is made. Same-origin absolute URIs and relative paths are unaffected; set the option to true to allow cross-origin service calls. The scheme/host/port comparison is factored into Consumer#cross_origin?, reused by the existing token-request redirect check. Adds spec/oauth/access_token_cross_origin_spec.rb. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n_spec Also merges latest main, whose re-baselined (now empty) rubocop-gradual lock resolves the stale lint-lock CI failure on this branch.
Member
|
Maintainer push (edits-from-maintainers is enabled on this PR):
CI is now green across the board (21 passing, JRuby/TruffleRuby matrix entries skip as usual). Local verification: full suite 198 examples, 0 failures; The change itself looks good — review notes on the design (all positive, two cosmetic nits) to follow separately. |
pboling
reviewed
Nov 8, 2026
pboling
left a comment
Member
There was a problem hiding this comment.
Review (verified locally at 2a31bd4)
Full suite on this branch: 198 examples, 0 failures (5 pending, optional deps). New spec: 7/7 pass. rubocop-gradual clean. The earlier CI failures were a stale lint lock from an outdated base, now resolved by the merge of main into this branch.
Design — looks sound
- Mirrors the existing
token_request_cross_origin_redirectshardening; reusing onecross_origin?/effective_portpair for both checks is the right factoring, and the rename oftoken_request_effective_portis self-contained (no other callers). - The refusal happens before the
consumer.uriswap, so a rejected call never mutates consumer state — and the spec proves restoration. - Comparison semantics are correct: scheme/host downcased, default ports normalize (
https://site:443≡https://site), userinfo tricks (https://site@attacker/) and trailing-dot hosts are refused, relative paths and same-origin absolute URIs are unaffected. - Defaulting
access_token_cross_origin_requeststofalseis consistent with the token-redirect option, and the CHANGELOG/README compatibility notes are clear.
Nits (non-blocking)
- In the spec,
"network reached: #{consumer.uri}"interpolates eagerly when the stub is defined, so the message always shows the original site URI. Harmless (assertions only match/network reached/), just slightly misleading when reading a failure. AccessToken#authority's comment says "port shown only when explicit", butURI::HTTP#port/URI::HTTPS#portalways answer a port, so theif uri.portbranch is always true for http/https URIs and the port always renders. Cosmetic — either drop the condition or adjust the comment.
pboling
previously approved these changes
Nov 8, 2026
- spec: make the Consumer#request stub message lazy (block form), so consumer.uri interpolates at call time — after any URI swap — instead of at stub definition; tighten the pass-through assertions to match the actual target URI the request would have gone to - AccessToken#authority: correct the comment — URI::HTTP/HTTPS always resolve a port, so it is not "shown only when explicit"
Member
|
Both review nits are addressed in f00b49c (maintainer push):
CI green across the matrix. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AccessToken#requesttemporarily swapsconsumer.urito any absolute URI passed to#get/#post/#put/#patch/#delete/#head, then signs and sends the OAuth request to that host. There is no check that the URI shares the configured:siteorigin.If a consuming application forwards an untrusted or user-influenced absolute URL into these helpers (e.g.
token.get(params[:callback_url])), the library will send the signed OAuthAuthorizationheader to an arbitrary host — disclosingoauth_consumer_key/oauth_tokento that host — or to an internal address (SSRF). The token/consumer secrets are never transmitted (they remain the HMAC key, and the signature is bound to the request URL), so this is an identifier/credential-header leak plus a request-forwarding primitive rather than a secret disclosure — but it still shouldn't happen silently by default.This is primarily an application-misuse hazard, but the library can make the safe behavior the default, exactly as it already does for token-endpoint redirects.
What this changes
Mirrors the existing
token_request_cross_origin_redirectshardening:access_token_cross_origin_requests(defaultfalse).:siteraisesOAuth::Errorbefore any request is made.:site, relative paths, or absolute URLs to the same host).trueto restore cross-origin service calls (for providers whose API endpoints legitimately live on a different origin than:site).The scheme/host/port comparison is factored into
Consumer#cross_origin?, which the existing token-request redirect check now reuses (no behavior change there).Compatibility note
This is a secure-by-default behavior change: code that today calls
AccessTokenhelpers with an absolute URL on a different origin than:sitewill start raisingOAuth::Erroruntil it setsaccess_token_cross_origin_requests: true. Relative paths and same-origin absolute URLs are unaffected. I've defaulted tofalseto match the philosophy oftoken_request_cross_origin_redirects— happy to flip the default totrue(opt-in hardening) if you'd prefer to avoid any default change.Tests
Adds
spec/oauth/access_token_cross_origin_spec.rb(7 examples): cross-origin by host / port / scheme are refused; same-origin absolute and relative paths pass through to the request layer;consumer.uriis restored after a refusal; and the opt-in allows cross-origin again. All pass locally.CHANGELOG and README updated.
🤖 Generated with Claude Code