Summary
<AutoConnect> trusts in-app-wallet auth material taken from the page URL on every load, with no check that the application initiated the flow. getUrlToken() reads authCookie, authResult, authProvider, authFlow, and walletId from the query string and from the hash fragment, then hands them to the connection flow. That flow persists the cookie and connects the browser to the wallet the token names, or, for authFlow=link, links the supplied profile onto the active account. There is no origin, nonce, state, or signature check anywhere on this path. After reading the params the SDK removes them from the address bar with pushState, so the user sees nothing.
This is the redirect-login version of a well-understood problem: accepting authorization-flow material that the current browser session did not initiate. The same SDK already guards its popup login against exactly this. loginWithOauth (in-app/web/lib/auth/oauth.ts:91) ignores any message whose event.origin is not thirdweb's own origin. The redirect path has no equivalent. getLoginPath.ts builds the return URL with walletId, authProvider, and authFlow and adds no state or nonce, so nothing ties a returned token to a login this app actually started.
Two impact directions
-
Account linking / takeover (authFlow=link). The intended sequence is stated in your own comment above the branch, autoConnectCore.ts:87:
// Handle linking flow: autoconnect with stored credentials, then link the new profile
So the victim is connected with their own stored credentials, and the URL-supplied profile is then linked via POST /api/2024-05-05/account/connect authenticated as the victim. Per thirdweb's own documentation for useLinkProfile, "when a profile is linked to the account, that profile can then be used to sign into the same account." A crafted link therefore attaches a login identity the attacker controls onto the victim's account. It is persistent and survives the session.
-
Unsolicited connection / session fixation (authCookie). saveAuthCookie(urlToken.authCookie) together with lastActiveWalletId = urlToken.walletId connects the visitor's browser to a wallet named by the link. Value the victim then deposits or approves sits under the attacker's wallet.
Preconditions are only that AutoConnect is active and the victim opens a link carrying walletId plus authCookie or authResult. The attacker supplies a token for an identity they control, obtained by an ordinary login. No forgery is required.
Reach: enabled by default, wider than <AutoConnect> alone
The URL-token path is enabled unless explicitly disabled (props.autoConnect !== false). That guard appears in five components: web ConnectButton, web ConnectEmbed, BuyWidget, and the React Native ConnectButton and ConnectModal. A bare <ConnectButton client={...} /> is affected, so the exposure is wider than apps that mount <AutoConnect> directly.
Where (v5.120.1, and unchanged on main as of 2026-07-30)
packages/thirdweb/src/wallets/in-app/web/lib/get-url-token.ts reads the five params from window.location.search and the hash. A search for nonce|state|origin|verify|signature in this file returns only the pushState that scrubs them.
packages/thirdweb/src/wallets/connection/autoConnectCore.ts routes authFlow === "link" to handleLinkingFlow (line 88), and authCookie to clientStorage.saveAuthCookie(...) (line 118), then overwrites lastActiveWalletId with the URL-supplied walletId (line 121). There is no provenance gate on this path.
packages/thirdweb/src/wallets/in-app/core/authentication/getLoginPath.ts builds redirectUrl with no state or nonce.
packages/thirdweb/src/wallets/in-app/web/lib/auth/oauth.ts shows the popup path's event.origin check, i.e. the guarantee the redirect path is missing.
The clientId domain allowlist does not mitigate this. The malicious link loads the integrator's own allowlisted origin, so every downstream request carries an allowed Origin natively.
Severity
The linking direction is an account-linking CSRF that leads to takeover, and the class is standardized. RFC 9700 (BCP 240) defines the threat in section 4.7 as "requests to the redirection endpoint that do not originate at the authorization server, but at a malicious third party," citing RFC 6819 section 4.4.1.8, "Threat: CSRF Attack against redirect-uri." It then states the requirement:
Clients that have ensured that the authorization server supports Proof Key for Code Exchange (PKCE) MAY rely on the CSRF protection provided by PKCE. In OpenID Connect flows, the nonce parameter provides CSRF protection. Otherwise, one-time use CSRF tokens carried in the state parameter that are securely bound to the user agent MUST be used for CSRF protection.
The redirect path here carries no such value.
Two published advisories with the same root cause, a state that is stateless or unbound to the initiating session, put a range on this:
- CVE-2024-56329 (Socialstream, GHSA-3q97-vjpp-c8rp), 8.9 High: account takeover through social-account linking with no user-consent step after the callback. Fixed by adding an explicit confirm-or-deny prompt.
- CVE-2025-68481 (fastapi-users, GHSA-5j53-63w8-8625), 5.9 Medium: one-click account takeover from a stateless login
state token.
Neither of those projects custodies value directly. That is the part that differs for a wallet SDK.
A shipped feature depends on this path
SiteLink and SiteEmbed deliberately place a live authCookie into a URL to hand a session to a sibling site. The consume side therefore cannot simply be removed. A provenance gate is the right fix.
One question for maintainers
Does the token-issuing server, or the /account/connect endpoint, bind the token to the session or device that initiated the flow, or reject linking an identity already attached to another account? If it does, that blunts the server side. If not, the client is the only line of defense and the gate below is required.
Requested change (any one helps, item 1 is the real fix)
- Provenance gate in the SDK, matching RFC 9700. On redirect initiation, persist a one-time value bound to this browser session and send it as
state. On return, honor a URL token only when it matches, otherwise ignore and strip it. This is the redirect analogue of the origin check the popup path already performs.
- An explicit opt-out, e.g.
<AutoConnect readUrlToken={false} /> or a wallet-level flag, so apps that use popup, OTP, or passkey only can disable URL-token consumption entirely.
- Explicit user confirmation before an account link that was initiated from a URL token, so a silent background link is not possible. This mirrors the fix accepted for CVE-2024-56329.
- At minimum, document that URL-token consumption is on by default and how to gate it.
Context
We have added an integrator-side mitigation (a one-time marker set at redirect initiation, used to strip any unmatched URL token before AutoConnect reads it). We are raising this because the control belongs in the SDK. Most integrators will not realize the URL path is load-bearing until they audit it, and the popup path shows the project already treats unsolicited auth material as untrusted elsewhere. Happy to share the approach or open a PR.
Summary
<AutoConnect>trusts in-app-wallet auth material taken from the page URL on every load, with no check that the application initiated the flow.getUrlToken()readsauthCookie,authResult,authProvider,authFlow, andwalletIdfrom the query string and from the hash fragment, then hands them to the connection flow. That flow persists the cookie and connects the browser to the wallet the token names, or, forauthFlow=link, links the supplied profile onto the active account. There is no origin, nonce,state, or signature check anywhere on this path. After reading the params the SDK removes them from the address bar withpushState, so the user sees nothing.This is the redirect-login version of a well-understood problem: accepting authorization-flow material that the current browser session did not initiate. The same SDK already guards its popup login against exactly this.
loginWithOauth(in-app/web/lib/auth/oauth.ts:91) ignores any message whoseevent.originis not thirdweb's own origin. The redirect path has no equivalent.getLoginPath.tsbuilds the return URL withwalletId,authProvider, andauthFlowand adds nostateor nonce, so nothing ties a returned token to a login this app actually started.Two impact directions
Account linking / takeover (
authFlow=link). The intended sequence is stated in your own comment above the branch,autoConnectCore.ts:87:// Handle linking flow: autoconnect with stored credentials, then link the new profileSo the victim is connected with their own stored credentials, and the URL-supplied profile is then linked via
POST /api/2024-05-05/account/connectauthenticated as the victim. Per thirdweb's own documentation foruseLinkProfile, "when a profile is linked to the account, that profile can then be used to sign into the same account." A crafted link therefore attaches a login identity the attacker controls onto the victim's account. It is persistent and survives the session.Unsolicited connection / session fixation (
authCookie).saveAuthCookie(urlToken.authCookie)together withlastActiveWalletId = urlToken.walletIdconnects the visitor's browser to a wallet named by the link. Value the victim then deposits or approves sits under the attacker's wallet.Preconditions are only that AutoConnect is active and the victim opens a link carrying
walletIdplusauthCookieorauthResult. The attacker supplies a token for an identity they control, obtained by an ordinary login. No forgery is required.Reach: enabled by default, wider than
<AutoConnect>aloneThe URL-token path is enabled unless explicitly disabled (
props.autoConnect !== false). That guard appears in five components: webConnectButton, webConnectEmbed,BuyWidget, and the React NativeConnectButtonandConnectModal. A bare<ConnectButton client={...} />is affected, so the exposure is wider than apps that mount<AutoConnect>directly.Where (v5.120.1, and unchanged on
mainas of 2026-07-30)packages/thirdweb/src/wallets/in-app/web/lib/get-url-token.tsreads the five params fromwindow.location.searchand the hash. A search fornonce|state|origin|verify|signaturein this file returns only thepushStatethat scrubs them.packages/thirdweb/src/wallets/connection/autoConnectCore.tsroutesauthFlow === "link"tohandleLinkingFlow(line 88), andauthCookietoclientStorage.saveAuthCookie(...)(line 118), then overwriteslastActiveWalletIdwith the URL-suppliedwalletId(line 121). There is no provenance gate on this path.packages/thirdweb/src/wallets/in-app/core/authentication/getLoginPath.tsbuildsredirectUrlwith nostateor nonce.packages/thirdweb/src/wallets/in-app/web/lib/auth/oauth.tsshows the popup path'sevent.origincheck, i.e. the guarantee the redirect path is missing.The clientId domain allowlist does not mitigate this. The malicious link loads the integrator's own allowlisted origin, so every downstream request carries an allowed
Originnatively.Severity
The linking direction is an account-linking CSRF that leads to takeover, and the class is standardized. RFC 9700 (BCP 240) defines the threat in section 4.7 as "requests to the redirection endpoint that do not originate at the authorization server, but at a malicious third party," citing RFC 6819 section 4.4.1.8, "Threat: CSRF Attack against redirect-uri." It then states the requirement:
The redirect path here carries no such value.
Two published advisories with the same root cause, a
statethat is stateless or unbound to the initiating session, put a range on this:statetoken.Neither of those projects custodies value directly. That is the part that differs for a wallet SDK.
A shipped feature depends on this path
SiteLinkandSiteEmbeddeliberately place a liveauthCookieinto a URL to hand a session to a sibling site. The consume side therefore cannot simply be removed. A provenance gate is the right fix.One question for maintainers
Does the token-issuing server, or the
/account/connectendpoint, bind the token to the session or device that initiated the flow, or reject linking an identity already attached to another account? If it does, that blunts the server side. If not, the client is the only line of defense and the gate below is required.Requested change (any one helps, item 1 is the real fix)
state. On return, honor a URL token only when it matches, otherwise ignore and strip it. This is the redirect analogue of the origin check the popup path already performs.<AutoConnect readUrlToken={false} />or a wallet-level flag, so apps that use popup, OTP, or passkey only can disable URL-token consumption entirely.Context
We have added an integrator-side mitigation (a one-time marker set at redirect initiation, used to strip any unmatched URL token before AutoConnect reads it). We are raising this because the control belongs in the SDK. Most integrators will not realize the URL path is load-bearing until they audit it, and the popup path shows the project already treats unsolicited auth material as untrusted elsewhere. Happy to share the approach or open a PR.