Skip to content

fix(client): don't follow cross-origin redirects with a bearer token #177

Closed

Description

@Bccorb

Found in the security review for #175 and #176.

In bearer transport (React Native), authorizedFetch and the client's calls under the adapter mount send the access token in Authorization. fetch follows redirects by default. If the application's API, or a misconfigured proxy in front of it, answers with a redirect to another origin, the request is sent on, and whether Authorization goes with it depends on the platform's networking stack:

  • Node 24's fetch (undici) and current Chrome, Firefox and Safari drop Authorization and Cookie on a cross-origin redirect, per the Fetch standard. That was tested against a local 302. x-seamless-auth-transport is kept.
  • OkHttp (Android) drops auth headers when the host changes.
  • iOS NSURLSession, which React Native's fetch uses on iOS, is unverified. It may carry custom headers across a redirect.

The #175 origin check judges only the URL the request starts at, not where a redirect takes it.

Proposed

In bearer transport, don't follow a redirect to an origin outside apiHost and trustedOrigins:

  • Default redirect: 'manual' or 'error' for bearer authorizedFetch and the adapter calls, unless the caller sets redirect explicitly. Then surface a clear error.
  • Or, as a fallback where the platform ignores redirect, reject when response.redirected and new URL(response.url).origin is not trusted. That detects the redirect after the fact but does not prevent the send.

Done when

  • The iOS behaviour is verified on a device or simulator with a test server that answers 302 to another origin.
  • Bearer requests never reach an untrusted origin with the access token, on iOS and Android.
  • There's a test in packages/client/tests/transport.node.test.ts, with the README updated.

Activity

  1. added
    bugSomething isn't working
    v0.19Targets the v0.19 release
    P1Next
    secureServes the most-secure promise
    on Oct 9, 2026
  2. added this to the v0.19 milestone on Oct 13, 2026
  3. added
    P2Soon
    and removed
    P1Next
    on Oct 17, 2026
  4. Bccorb commented on Oct 20, 2026

    @Bccorb
    ContributorAuthor

    Verified on iOS (2026-10-20): iPhone 17 Pro simulator, iOS 26.5, Expo Go SDK 57, React Native 0.86.3, @seamless-auth/client 0.7.0 (with #192). A probe app ran against two local servers: localhost:5700 answering 302 to 127.0.0.1:5701, and the other origin logging every Authorization it received.

    Check Result
    Raw fetch with Authorization, cross-origin 302 Followed to 127.0.0.1:5701; the other origin received no Authorization. NSURLSession strips it on a cross-origin hop here
    Raw fetch, redirect: 'manual' Honoured: status 302 returned, not followed
    Client bearer authorizedFetch, cross-origin 302 Rejected with UntrustedRedirectError; the other origin received no request at all
    Client bearer authorizedFetch, same-origin 302 Followed; the landing route received Bearer client-token

    So on this iOS and React Native version the token cannot reach another origin through a redirect, whether or not the client intervenes, and the client refuses the hop before sending anything.

    One gap found: on React Native a followed redirect reports response.redirected === false while response.url is the new origin. The client's after-the-fact check keys on redirected, so it would miss a redirect on a platform that ignores redirect: 'manual'. React Native 0.86 honours manual, so it does not bite here, but the check should compare response.url with the requested URL instead. Not yet verified: Android (OkHttp is documented to drop auth headers on a host change) and older React Native versions.

  5. Bccorb commented on Oct 20, 2026

    @Bccorb
    ContributorAuthor

    Closing: fixed in #192 (client 0.7.0) and verified on iOS 26.5 / React Native 0.86.3 in the comment above. The after-the-fact check gap found there is tracked separately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2SoonbugSomething isn't workingsecureServes the most-secure promisev0.19Targets the v0.19 release

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions