Repository navigation
fix: v0.20 client hardening (#198, #199, #204, #205, #206, #207) - #209
Merged
Merged
Conversation
React Native's fetch follows redirects itself and never sets response.redirected, so a response that ended on another origin passed the guard. Refuse any response whose url differs from the one requested and is not trusted, and document that on React Native the guard detects a followed redirect rather than preventing it. Closes #198
A request that threw reached the app as a status 0 error with no cause, so an UntrustedOriginError or UntrustedRedirectError could not be told apart from a network failure. Pass the thrown error through as the cause. Closes #199
The refresh ran outside the origin-checked request loop with the default redirect: 'follow', so a 3xx would resend the refresh token to the target. Send it with redirect: 'manual', refuse any 3xx or hidden redirect, apply the final-URL check for platforms that follow redirects themselves, and clear the held session when the refresh is refused this way. Closes #204
VerifyMagicLink and OAuthCallback left ?token= and ?code= in the address bar and history. Replace the query once it is read, as the Vue, Angular and Svelte screens do with dropQuery(). The magic link token is kept in state so a held sign-in can still be confirmed after the query is gone. Closes #205
The browser and React Native redirect ports opened whatever URL the server returned. Allow https, and http only on a loopback host, judging the scheme the way a URL parser does (case, tabs, newlines, leading control characters), and throw UnsafeAuthorizationUrlError otherwise. startOAuthSignIn reports that as a provider that would not start. Closes #207
The release job ran npm ci with lifecycle scripts while holding an OIDC token and the npm token, and granted issues: write it never used. - install with npm ci --ignore-scripts; no package needs an install script, and the existing build step builds every package - default the workflow to no permissions and grant the job only contents, pull-requests and id-token write - pin actions/checkout, actions/setup-node and changesets/action to the commit SHAs their current major tags resolve to Closes #206
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.
Six v0.20 hardening fixes for the client core, the React and React Native bindings, and the release workflow. One commit per issue, each with tests and (where user-facing) a patch changeset.
Fixes
response.redirectedwas true, whichwhatwg-fetchnever sets. A bearer request now rejects withUntrustedRedirectErrorwheneverresponse.urlis set, differs from the requested URL, and is not onapiHostortrustedOrigins. The client and React Native READMEs now say that on React Native the guard detects a followed redirect after the fact rather than preventing it. Test: a fetch stub with noredirected,urlon another origin, status 200.requestResultkeeps the cause. A thrown request now yields a status0SeamlessAuthErrorwhosecauseis the thrown error. Test: a client method given a fetch that throwsUntrustedRedirectErrorreturns an error whosecauseis that instance.POST /refreshis sent withredirect: 'manual'. Any 3xx, anopaqueredirect, or a response whose final URL is on an untrusted origin rejects withUntrustedRedirectErrorand clears the held session (it cannot be renewed through that server, and on React Native the token may already have gone). Tests cover a cross-origin 307, a same-origin 302, a hidden redirect, and a platform-followed redirect.VerifyMagicLinkandOAuthCallbackreplace the URL query once read (setSearchParams(..., { replace: true })), as the Vue, Angular and Svelte screens do withdropQuery(). The magic link token is held in state so a sign-in held for another device can still be confirmed. Tests include a realMemoryRouterrun showing the query is gone and confirmation still sends the token.npm ci --ignore-scripts(no package needs an install script; the existing build step builds everything), workflow-levelpermissions: {}with the job granted onlycontents,pull-requestsandid-tokenwrite (issues: writedropped), andactions/checkout,actions/setup-nodeandchangesets/actionpinned to the SHAs their currentv6/v6/v1refs resolve to, with version comments.assertNavigableAuthorizationUrlandUnsafeAuthorizationUrlErrorin@seamless-auth/client. The browser port and the React NativecreateWebBrowserOAuthRedirectport allow onlyhttps:, orhttp:onlocalhost,127.0.0.1or[::1], and throw otherwise without navigating. The scheme is judged the way a URL parser does (case, embedded tabs and newlines, leading control characters), by hand because React Native's built-inURLhas not always implementedprotocol/hostname.startOAuthSignInreports a refused URL as a provider that could not start. Tests coverjavascript:,data:, mixed case, tab/newline-split and percent-encoded variants, relative URLs, non-loopbackhttp:, and userinfo/backslash host tricks.Checks
Run on a fresh
npm ci --ignore-scriptsinstall, matching the new release job:Closes #198
Closes #199
Closes #204
Closes #205
Closes #206
Closes #207