Skip to content

fix: canonicalize typed-sign data to prevent display/signing divergence#33187

Open
jpuri wants to merge 4 commits into
mainfrom
fix/canonicalize-typed-sign-data
Open

fix: canonicalize typed-sign data to prevent display/signing divergence#33187
jpuri wants to merge 4 commits into
mainfrom
fix/canonicalize-typed-sign-data

Conversation

@jpuri

@jpuri jpuri commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Description

Improve typed sign validation in mobile.

Changelog

CHANGELOG entry:

Related issues

Fixes: https://consensyssoftware.atlassian.net/browse/CONF-1655

Manual testing steps

NA

Screenshots/Recordings

NA

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics

For performance guidelines and tooling, see the Performance Guide.

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.

Note

High Risk
Touches the signing path for typed-data RPC methods; incorrect canonicalization could change signed payloads, though the change is narrow and covered by regression tests.

Overview
Fixes a display vs signing mismatch for EIP-712 typed data on mobile by no longer forwarding the raw dapp JSON string into SignatureController.

canonicalizeTypedMessageData round-trips the payload through JSON.parse / JSON.stringify (invalid JSON is left unchanged). Duplicate keys collapse to the last value, aligning what the UI can derive with what signing uses—matching the extension’s normalizeTypedMessage behavior.

generateRawSignature now passes canonicalized data to newUnsignedTypedMessage for both v3 and v4.

New middleware tests cover malicious duplicate-key payloads (e.g. two message.value entries separated by nested fields) for eth_signTypedData_v3 and eth_signTypedData_v4.

Reviewed by Cursor Bugbot for commit 291bc80. Bugbot is set up for automated code reviews on this repo. Configure here.

Duplicate JSON keys in eth_signTypedData_v3/v4 payloads could cause the
UI regex-based value extraction to show a different amount than what
JSON.parse produces at signing time. A malicious dapp could exploit this
by placing a small decoy value before a nested object boundary and a
large real value after it.

Canonicalize the typed-data JSON via JSON.parse/JSON.stringify in
generateRawSignature before passing it to SignatureController. This
mirrors the normalizeTypedMessage step the extension performs via
@metamask/eth-json-rpc-middleware.
@jpuri jpuri requested a review from a team as a code owner July 13, 2026 10:05
@jpuri jpuri added team-confirmations Push issues to confirmations team no-changelog no-changelog Indicates no external facing user changes, therefore no changelog documentation needed labels Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@jpuri jpuri enabled auto-merge July 13, 2026 10:07
@github-actions github-actions Bot added size-M risk:medium AI analysis: medium risk labels Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Flaky unit test detection

Run history flaky detection

View recent run history

Historical failure rate is a hint, not proof — review each suggestion in context. See the flaky-test-detection skill for the full pattern reference and manual audit workflow.

Failures / runs sampled per window:

File 7d 15d 30d
app/core/RPCMethods/RPCMethodMiddleware.test.ts 0/50 0/178 0/377

AI-detected flaky patterns

app/core/RPCMethods/RPCMethodMiddleware.test.ts

  • J3 — Missing jest.clearAllMocks()/resetAllMocks() between tests (high)
    • Multiple tests inside this describe (and nested ones) set custom mock implementations (e.g. MockEngine.context.PermissionController.revokePermissions.mockImplementation(() => { throw new Error(...) }), mockAddTransaction.mockImplementation, and similar for other mocks). The beforeEach only uses clearAllMocks() which does not reset implementations. Without resetAllMocks() in afterEach, mock state can leak across tests, leading to intermittent failures that depend on test execution order. This matches J3 exactly. Historical data showed zero failures so far, but the pattern is present and risky for CI.
    • Suggested fix in app/core/RPCMethods/RPCMethodMiddleware.test.ts:
      -describe('getRpcMethodMiddleware', () => {
      -  beforeEach(() => jest.clearAllMocks());
      -  it('allows unrecognized methods to pass through without PermissionController middleware', async () => {
      +describe('getRpcMethodMiddleware', () => {
      +  beforeEach(() => jest.clearAllMocks());
      +  afterEach(() => {
      +    jest.resetAllMocks();
      +  });
      +  it('allows unrecognized methods to pass through without PermissionController middleware', async () => {
  • J10 — jest.spyOn() without restoreAllMocks()/mockRestore() afterward (medium)
    • jest.spyOn is used on Engine.context.PermissionController.requestPermissions (and also on PPOMUtil.validateRequest in the typed message tests). No afterEach uses jest.restoreAllMocks() (or per-spy .mockRestore()). The spy wrapper will persist and alter behavior of subsequent tests that call the real method, creating order-dependent flakiness. Matches J10. No timers or React act() issues found in this RPC test file.
    • Suggested fix in app/core/RPCMethods/RPCMethodMiddleware.test.ts:
      -      const requestPermissionsSpy = jest.spyOn(
      -        Engine.context.PermissionController,
      -        'requestPermissions',
      -      );
      -      mockGetPermittedAccounts.mockReturnValue([addressMock]);
      +      const requestPermissionsSpy = jest.spyOn(
      +        Engine.context.PermissionController,
      +        'requestPermissions',
      +      );
      +      mockGetPermittedAccounts.mockReturnValue([addressMock]);
      +
      +      // ... existing test body ...
      +
      +      // At end of test or better in afterEach:
      +      requestPermissionsSpy.mockRestore();

This check is informational only and does not block merging.

@github-actions github-actions Bot added risk:low AI analysis: low risk and removed risk:medium AI analysis: medium risk labels Jul 13, 2026
@matthewwalsh0 matthewwalsh0 self-requested a review July 13, 2026 14:00
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeConfirmations
  • Selected Performance tags: None (no tests recommended)
  • Risk Level: medium
  • AI Confidence: 92%
click to see 🤖 AI reasoning details

E2E Test Selection:
The PR modifies RPCMethodMiddleware.ts to add a canonicalizeTypedMessageData() function that normalizes typed data JSON strings via JSON.parse/JSON.stringify before passing them to SignatureController.newUnsignedTypedMessage(). This is applied specifically to eth_signTypedData_v3 and eth_signTypedData_v4 requests. The change is a security fix preventing duplicate JSON key attacks where display and signing could diverge.

The test file adds unit tests covering: (1) duplicate key stripping, (2) last-occurrence value resolution, and (3) idempotent re-parsing.

E2E impact: The SmokeConfirmations tag directly covers typed signature flows (V1, V3, V4) as confirmed by tests/smoke/confirmations/signatures/signatures-typed.spec.ts which tests TypedV1Sign, TypedV3Sign, and TypedV4Sign flows. The changed code path is exercised when a dApp sends eth_signTypedData_v3 or eth_signTypedData_v4 requests through the RPC middleware.

No other user flows are affected: this is a targeted change in the RPC middleware layer for typed data signatures only. No UI components, navigation, account management, network selection, swap/stake/bridge flows, or browser functionality are modified.

Performance Test Selection:
The change adds a lightweight JSON.parse/JSON.stringify round-trip for typed data strings in the RPC middleware. This is a negligible computational operation that occurs only during signature requests (not during app launch, login, asset loading, swaps, or onboarding). No performance-sensitive code paths are affected.

View GitHub Actions results

@sonarqubecloud

Copy link
Copy Markdown

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

Labels

no-changelog no-changelog Indicates no external facing user changes, therefore no changelog documentation needed risk:low AI analysis: low risk size-M team-confirmations Push issues to confirmations team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants