Skip to content

fix: prevent EIP-712 permit display value spoofing via decoy nested field#965

Open
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1782242337-fix-permit-value-spoofing
Open

fix: prevent EIP-712 permit display value spoofing via decoy nested field#965
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1782242337-fix-permit-value-spoofing

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 23, 2026

Copy link
Copy Markdown

Description

Security fix. The typed-data (EIP-712) permit confirmation UI could display a token amount that diverges from the amount actually being signed, letting a malicious dApp trick a user into approving a large/unlimited allowance while the wallet shows a tiny one.

Root cause (app/components/Views/confirmations/utils/signature.ts): to preserve precision for integers larger than Number.MAX_SAFE_INTEGER, parseAndNormalizeSignTypedData overwrote result.message.value with the result of a regex run over the raw request string:

const REGEX_MESSAGE_VALUE_LARGE = /"message"\s*:\s*\{[^}]*"value"\s*:\s*(\d{15,})/u;

[^}]* is greedy and backtracks to the last "value":<15+ digits> before the first }. A dApp can append a decoy nested struct so the regex captures the decoy instead of the real top-level permit value:

message = {"value":900000000000000000000,"a":{"value":100000000000000}}
// JSON.parse -> real value 9e20 (correct, just lossy)
// regex      -> 100000000000000  (decoy, used for display)

The decoy field isn't in types[primaryType], so it's stripped from the EIP-712 hash and ignored by the signer — the user sees ~0.0001 tokens but signs 9e20. Both confirmation surfaces (the message DataTree/TokenValue and the permit simulation) consume this same poisoned value, so no field shows the true amount.

Solution

Stop reconstructing the displayed value from a separate regex over the raw text. The native JSON.parse already reads the correct top-level message.value (it only loses precision by coercing to scientific notation). So we derive the precise string from a second, structurally-identical parse of the same payload with every numeric literal rewritten as a quoted string:

// new helper: quotes number *values* only (string contents/keys untouched),
// so JSON.parse yields the same object tree with numbers as exact-digit strings
function stringifyJsonNumbersAsStrings(source) { ... }

export const parseAndNormalizeSignTypedData = (messageParamsData) => {
  const result = JSON.parse(messageParamsData);
  if (result.message?.value) {
    const precise = JSON.parse(stringifyJsonNumbersAsStrings(messageParamsData))?.message?.value;
    result.message.value = precise == null ? String(result.message.value) : String(precise);
  }
  return result;
};

Because the precise value comes from a real parse at the correct structural position, it always equals the value JSON.parse (and the signer) sees — a decoy nested value, key reordering, or a value substring inside another string field can no longer divert the displayed amount. Large-integer precision is still preserved (the original motivation). REGEX_MESSAGE_VALUE_LARGE / extractLargeMessageValue are removed.

Only parseAndNormalizeSignTypedData is touched; the existing truthy guard (if (result.message?.value)) and all consumers are unchanged.

Related issues

Fixes: EIP-712 permit confirmation displays attacker-spoofable token value (display diverges from signed amount)

Manual testing steps

  1. From a dApp, send eth_signTypedData_v4 for an EIP-2612 Permit whose message is {"value":900000000000000000000,"a":{"value":100000000000000}}.
  2. Open the signature confirmation in MetaMask Mobile.
  3. Verify both the message data tree and the permit simulation show the real allowance (9e20), not the decoy 1e14.

Screenshots/Recordings

Before

Displayed/simulated allowance shows the tiny decoy amount while the signed value is large/unlimited.

After

Displayed/simulated allowance matches the actual signed message.value.

Pre-merge author checklist

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.

Link to Devin session: https://app.devin.ai/sessions/d17444b2cbcd48a78d98e35222a35ed8
Requested by: @kylie-chang


Devin Review

Status Commit
⚪ Not started

Run Devin Review

Open in Devin Review (Staging)

…ield (bug)

Co-Authored-By: Kylie Chang <kylie.chang@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants