Skip to content

Commit 4a0a472

Browse files
authored
fix: WalletConnect sign requests show garbled origin in 'Request from' (#29102)
## **Description** When a dapp connects via WalletConnect v2, the "Request from" field on sign request confirmation screens occasionally renders a random long string (or empty value) instead of the dapp's domain. This causes confirmations to look untrustworthy and triggers "malicious/deceptive" warnings — reported in the wild against `merchant.cray.pro`. Root cause: `getUnverifiedRequestOrigin` in `app/core/WalletConnect/wc-utils.ts` used a nullish-coalescing fallback on `request.verifyContext?.verified?.origin`. The WalletConnect Verify API can legitimately return a non-`null`/`undefined` value that is not a URL (an empty string, or a non-URL identifier when the dapp is unverified or initiated from a non-browser context). `??` does not fall back on empty or non-URL strings, so the bogus value was rendered verbatim in the UI. Fix: only trust `verified.origin` when it parses as a URL (`isValidUrl`). Otherwise fall back to `session.peer.metadata.url` — the same fallback already used when `verifyContext` is entirely absent. No new behavior, just a tighter validity check on the existing code path. The change is isolated to one pure utility function and is covered by new unit tests that characterize both the bug and the fix (they fail against the prior implementation). ## **Changelog** CHANGELOG entry: Fixed WalletConnect sign requests occasionally displaying a garbled string in the "Request from" field instead of the dapp's domain. ## **Related issues** Fixes: [WAPI-1449](https://consensyssoftware.atlassian.net/browse/WAPI-1449) ## **Manual testing steps** The originally reported dapp (`merchant.cray.pro`) is unreliable, so the authoritative verification path is the unit tests added in this PR. A happy-path smoke test is also recommended to confirm no regression on well-behaved dapps. ```gherkin Feature: WalletConnect sign request origin display Scenario: happy-path dapp still shows its real domain Given a user with MetaMask Mobile installed And a well-behaved WalletConnect v2 dapp (e.g. react-app.walletconnect.com) When the user connects via WalletConnect and triggers a personal_sign request Then the confirmation sheet's "Request from" row displays the dapp's domain And no regression is observed versus main Scenario: dapp with an invalid verifyContext origin falls back gracefully Given a WalletConnect v2 session where verifyContext.verified.origin is empty or a non-URL string When the user triggers a sign request Then the confirmation sheet's "Request from" row displays session.peer.metadata.url And no random identifier is rendered ``` Unit tests: ``` yarn jest app/core/WalletConnect/wc-utils.test.ts ``` All 42 tests pass with the fix. The 3 bug-characterization tests (empty string, non-URL string, bare hostname) fail when the fix is reverted, confirming they protect against regression. ## **Screenshots/Recordings** N/A — cannot reliably reproduce the reported UI without the affected dapp. Unit tests deterministically cover the behavior change. ### **Before** Confirmation's "Request from" field showed a random long string for affected dapps. ### **After** Falls back to `session.peer.metadata.url`, so "Request from" shows the dapp's self-reported URL instead of a garbled value. ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. [WAPI-1449]: https://consensyssoftware.atlassian.net/browse/WAPI-1449?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes the logic used to display a WalletConnect request’s origin; while small and well-tested, it affects a security-sensitive confirmation UI field and could impact what users see when approving signatures. > > **Overview** > Prevents WalletConnect signing confirmations from showing garbled/empty “Request from” values by only using `request.verifyContext.verified.origin` when it’s a valid, parseable URL; otherwise it falls back to the existing `defaultOrigin` (peer metadata URL). > > Adds unit tests for `getUnverifiedRequestOrigin` covering valid URL, empty string, non-URL identifier, missing `verifyContext`, and bare-hostname cases to lock in the new fallback behavior. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit dadb5ec. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent dcad69f commit 4a0a472

2 files changed

Lines changed: 75 additions & 1 deletion

File tree

app/core/WalletConnect/wc-utils.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import {
1010
getHostname,
1111
normalizeDappUrl,
1212
isValidUrl,
13+
getUnverifiedRequestOrigin,
1314
} from './wc-utils';
15+
import type { WalletKitTypes } from '@reown/walletkit';
1416
import type {
1517
NavigationContainerRef,
1618
ParamListBase,
@@ -392,4 +394,66 @@ describe('WalletConnect Utils', () => {
392394
);
393395
});
394396
});
397+
398+
describe('getUnverifiedRequestOrigin', () => {
399+
const defaultOrigin = 'https://dapp.example.com';
400+
401+
const makeRequest = (origin: unknown) =>
402+
({
403+
verifyContext: { verified: { origin } },
404+
}) as unknown as WalletKitTypes.SessionRequest;
405+
406+
it('returns the verified origin when it is a valid URL', () => {
407+
expect(
408+
getUnverifiedRequestOrigin(
409+
makeRequest('https://some.example.com'),
410+
defaultOrigin,
411+
),
412+
).toBe('https://some.example.com');
413+
});
414+
415+
it('falls back to defaultOrigin when verified origin is an empty string', () => {
416+
expect(getUnverifiedRequestOrigin(makeRequest(''), defaultOrigin)).toBe(
417+
defaultOrigin,
418+
);
419+
});
420+
421+
it('falls back to defaultOrigin when verified origin is a non-URL string', () => {
422+
expect(
423+
getUnverifiedRequestOrigin(
424+
makeRequest('a7c9f3e1b8d6c2f4e9a1b3d5c7e9f1a3'),
425+
defaultOrigin,
426+
),
427+
).toBe(defaultOrigin);
428+
});
429+
430+
it('falls back to defaultOrigin when verified origin is missing', () => {
431+
expect(
432+
getUnverifiedRequestOrigin(
433+
{} as WalletKitTypes.SessionRequest,
434+
defaultOrigin,
435+
),
436+
).toBe(defaultOrigin);
437+
});
438+
439+
it('falls back to defaultOrigin when verifyContext is missing', () => {
440+
expect(
441+
getUnverifiedRequestOrigin(
442+
{
443+
verifyContext: undefined,
444+
} as unknown as WalletKitTypes.SessionRequest,
445+
defaultOrigin,
446+
),
447+
).toBe(defaultOrigin);
448+
});
449+
450+
it('falls back to defaultOrigin when verified origin is a bare hostname (no protocol)', () => {
451+
expect(
452+
getUnverifiedRequestOrigin(
453+
makeRequest('some.example.com'),
454+
defaultOrigin,
455+
),
456+
).toBe(defaultOrigin);
457+
});
458+
});
395459
});

app/core/WalletConnect/wc-utils.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,4 +416,14 @@ export const hasPermissionsToSwitchChainRequest = async (
416416
export const getUnverifiedRequestOrigin = (
417417
request: WalletKitTypes.SessionRequest,
418418
defaultOrigin: string,
419-
) => request.verifyContext?.verified?.origin ?? defaultOrigin;
419+
) => {
420+
// Only trust verifyContext.verified.origin when it's a parseable URL. The
421+
// WalletConnect Verify API may return an empty string or a non-URL value
422+
// (e.g. a topic/identifier) when the dapp is unverified, which would
423+
// otherwise be rendered verbatim in the "Request from" field.
424+
const verifiedOrigin = request.verifyContext?.verified?.origin;
425+
if (isValidUrl(verifiedOrigin)) {
426+
return verifiedOrigin as string;
427+
}
428+
return defaultOrigin;
429+
};

0 commit comments

Comments
 (0)