Skip to content

fix(react-native-ui): accept offramp wallet requests only from the main frame (OFFRAMP-731) - #2293

Open
AngelPaella wants to merge 4 commits into
angelcalderaro/offramp-664-sign-messagefrom
angelcalderaro/offramp-731-rn-main-frame-nonce
Open

AngelPaella wants to merge 4 commits into
angelcalderaro/offramp-664-sign-messagefrom
angelcalderaro/offramp-731-rn-main-frame-nonce

Conversation

@AngelPaella

@AngelPaella AngelPaella commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Linear: https://linear.app/crossmint/issue/OFFRAMP-731 (interim fix; the issue stays open)

On Android, react-native-webview gives window.ReactNativeWebView.postMessage to every frame, and the message's url is either the sender's origin (WebMessageListener) or the main page's URL (legacy addJavascriptInterface). So isOfframpPage can't tell which frame posted. The offramp page now hosts a cross-origin frame (Persona's capture widget during KYC), and on the legacy path its messages pass the check. A frame could then send crypto:send-transaction or crypto:sign-message and have the host's payer sign.

This PR does what Flutter already does (Crossmint/crossmint-flutter-sdk#117), with no change to the page:

  • After each main-frame load of this offramp session, the component injects a wrapper into the main frame with injectJavaScript (main frame only on both platforms). It stamps every message with a fresh random nonce, __mainFrameNonce (16 bytes from react-native-get-random-values).
  • crypto:send-transaction and crypto:sign-message without the current nonce are dropped with a console.warn and no reply, since a reply would reach the main frame, not the sender. Other events pass as before.
  • It stamps only a load whose URL is the offramp route with this component's own apiKey and credentials, so a frame can't navigate the top frame to another session and get it stamped. Any other load clears the nonce and logs a warning. The two params are read without URL.searchParams, which older supported React Native versions don't implement (sameSession.ts).
  • The nonce survives onLoadStart. On Android that event comes from same-document pushState/replaceState, which the page's router does on hydration, with no onLoad after it. Only onLoad rotates the nonce.
  • Pending wallet requests now reset on offramp:ready (each new document sends it once), not on onLoadStart, as feat(offramp): payer.signMessage for in-iframe wallet ownership proof (OFFRAMP-664) #2266 already does on web. Before, a replaceState while a signature was pending dropped the wallet's reply, and the page waited 120 s for a transaction or forever for a message signature. This also fixes crypto:send-transaction as it is on main.

Stacked on #2266 (OFFRAMP-664), which adds crypto:sign-message.

What it leaves open, for OFFRAMP-731's contract-level fix:

  • A forged offramp:completed (other events aren't checked).
  • Same-origin (crossmint.com) frames, which can call the main frame's wrapper.
  • Wallet requests sent before onLoad, or after a failed injection, are dropped, as in Flutter. Requests are user-initiated, so they normally come after the load.

Test plan

  • packages/client/ui/react-native, crossmint-embedded-offramp.test.tsx: wallet requests with the current nonce are answered; without it, with a wrong, null or missing one, before any load, and with the previous load's nonce are dropped with no reply; the nonce survives onLoadStart; non-wallet events pass without it; another session (other credentials or apiKey) and a non-offramp page get no nonce and a warning; the session check works with URL stubbed to throw and handles +/%20, param order, fragments, extra or missing params and malformed escapes; the injected script stamps, keeps the newer nonce on a second stamp, forwards unparsable messages, and posts once; pending send and sign replies still arrive after onLoadStart, and after offramp:ready an old reply is dropped while a new request works. 8 files, 115 tests passed.
  • Mutation-checked: removing the gate, the null guard, the session check (each param), the onLoad clear, the spread order, the + decoding or the fragment split, moving the post into the try, re-adding an onLoadStart nonce clear or pending reset, or removing the offramp:ready reset each fails a named test.
  • tsc --noEmit shows no errors in the touched files; biome shows only test-file warnings.
  • Device check (staging, 2026-11-06), Release builds of apps/wallets/expo with react-native-webview 14.0.1 on an iOS 26.5 simulator and an Android 16 emulator (WebView 133, WebMessageListener bridge). iOS: the ownership proof (crypto:sign-message) and the deposit (crypto:send-transaction) from the offramp page reached the payer; the deposit was declined on purpose and the page recovered. Android: the same for the deposit (the wallet was already verified). A same-origin https frame's own bridge request was dropped by the gate on Android; iOS frames have no bridge. A frame calling the main page's bridge was accepted, the same-origin residual above. Loading a frame did not reset the nonce. Not covered: the legacy addJavascriptInterface bridge, which needs an old Android WebView.

Package updates

.changeset/: patch for @crossmint/client-sdk-react-native-ui.

…navigation and stamp only the own session (OFFRAMP-731)
…rchParams and warn when a load is not stamped (OFFRAMP-731)
@changeset-bot

changeset-bot Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5405be7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@crossmint/client-sdk-react-native-ui Patch
@crossmint/wallets-playground-expo Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@AngelPaella
AngelPaella marked this pull request as ready for review November 6, 2026 18:40
@greptile-apps

greptile-apps Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge within its stated interim scope.

Reviews (1) · Last reviewed commit: "fix(react-native-ui): reset offramp wall..." · Reviewed by Greptile

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant