Home - Coinspect Security
EIP 712 chainId implementation issue

A UI Flaw in Top Crypto Wallets We Need to Address

Security Engineer
Security, Wallets, EVM, EIP-712

In this post of our ongoing series on crypto wallet research, we uncover a text injection vulnerability in the message-signing mechanisms of over 27 software wallets. As illustrated in the following picture, this flaw allowed malicious DApps to distort the user’s perception of the safety of the signing operation requested, ultimately seizing victims’ crypto assets.

Fake transaction simulation

Fake transaction simulation message injected in the wallet UI

In late 2022, Coinspect initiated a project on software wallets. Our blockchain security specialists examined risks across vendors and studied prevalent architectural patterns. The goals? To develop a thorough threat model and highlight top security practices.

It’s no secret that social engineering is the primary source of most attacks to crypto users, rather than the exploitation of complex bugs. @tayvano_ from Metamask has repeatedly demonstrated that phishing is the primary method chosen by attackers, leading to massive losses.

While many teams quickly addressed the vulnerability, it highlighted a troubling trend. Users across various wallets seem to be unprotected against numerous social engineering tactics, and the multiple responses we got from vendors is that users should avoid engaging with malicious DApps. Contrary to what many wallet vendors might think, the responsibility rests with the wallets themselves to focus on prevention and proactive protection, rather than unintentionally aiding hackers by enabling phishing scenarios.

The path to better user protection begins with recognizing that most social engineering attacks likely start with malicious websites, making them one of the biggest threats to wallets. This requires multiple security measures: website ratings based on metrics like age and popularity, robust transaction simulations with fail-safe, and advanced message parsing, among others. Therefore, we look forward to engaging in discussions to further security and user experience in the software wallets sector.

The significance of this finding, similarly to the one discovered back in February 2023, is underscored by the extensive number of vendors involved, in addition to the potential impact of the bug itself. Observing standard disclosure protocols, we quickly informed the affected vendors, allowing them enough time to manage the issue.

Affected vendors

This UI injection vulnerability impacted over 27 crypto software wallets. The list of impacted vendors might be longer, as we primarily contacted mainstream ones directly. Nonetheless, we also offered to check this vulnerability for any other vendor. Below it is presented a summary of the disclosure process along with information about the impacted vendors, dates and fix status of each report.

Vendor Status Submitted on Triage days Fix days
1inch Not accepted 15-may-23 N/A N/A
Ambire Not accepted 17-may-23 N/A N/A
Argent Pending fix 12-may-23 20 N/A
Bitizen Fixed 13-may-23 2 30
Bitkeep Fixed 15-may-23 1 9
Blockchain.com Not accepted 15-may-23 N/A N/A
Blockwallet Not accepted 12-may-23 12 N/A
Brave Fixed 15-may-23 1 37
Coinbase Fixed 12-may-23 3 10
Coolwallet Fixed 31-may-23 20 44
Core Pending fix 12-may-23 5 N/A
Crypto.com Fixed 12-may-23 3 N/A
Exodus Fixed 12-may-23 5 10
HashKey Me Not accepted 15-may-23 N/A N/A
imToken Pending fix 13-may-23 16 N/A
MyEtherWallet Pending fix 12-may-23 28 N/A
OKX Pending fix 15-may-23 45 N/A
OneKey Fixed 12-may-23 30 66
Phantom Fixed 12-may-23 13 N/A
Rabby Fixed 12-may-23 3 9
Robinhood Fixed 15-may-23 0 2
Tally Ho Fixed 16-may-23 0 1
Trust Wallet Not accepted 12-may-23 N/A N/A
Uniswap Not accepted 15-may-23 N/A N/A
Unstoppable Not accepted 13-may-23 5 N/A
ZenGo Fixed 12-may-23 12 56
Zerion Not accepted 13-may-23 11 N/A

From the previous table, it took vendors an average of 11 days to triage the report. On the other hand, those who fixed the vulnerability took on average a month to release the patch. It is worth noticing the remarkable job done by a few teams who triaged and fixed the problem in such a short time frame.

Technical summary

A text injection vulnerability in the EIP-712 signature process allowed malicious DApps to trick users into signing an EIP-712 object different from the one presented in the signature approval preview. Consequently, users were at risk of unknowingly transferring control of their ERC-20 tokens, NFTs, or Uniswap positions to adversaries by signing hidden Permit messages.

This was enabled by the lack of sanitization of the EIP-712 object received via the signTypedData function, allowing adversaries to inject data in the message property of the object. The injected data was then ignored by the wallet when computing the signature as it didn’t match any type in the types property of the EIP-712 object.

Below, we present a screenshot from one of the vulnerable wallets, which worryingly, the vendor did not recognize as a vulnerability. The example shows a fake, injected transaction simulation message followed by multiple whitespaces, and the real message to be signed at the bottom of the screen.

Injection Example

Then, if we scroll down to the bottom of the message, we can discover the hidden or malicious message, buried beneath an endless number of whitespaces.

Injection Example

Adversaries could have abused this bug to trick users into signing malicious, hardly noticeable EIP-712 objects placed at the bottom of the signature preview with the help of whitespaces. To increase the probabilities of succeeding, they could have harnessed Unicode control characters to make the attack look more enticing.

The fix

The solution is straightforward, it simply involves filtering out fields in the message that don’t match with the formats specified in types. However, some vendors also opted other mitigation strategies to help users detect such attacks, like forcing scrolling to the message’s end before signing, displaying Unicode characters differently, and counting message fields to name a few.

Interested in learning more about the vulnerability and its attack vector? Feel free to continue reading.

EIP-712 Overview

EIP-712 defines a way to cryptographically hash and sign a typed JSON data structure. EIP-712 allows creating structured, human-readable representations of typed data that users can sign with their private keys, which can then be verified on-chain by a smart contract.

An example EIP-712 object looks like the following:

{
   "types": {
       "EIP712Domain": [
           { "name": "name", "type": "string" },
           { "name": "version", "type": "string" },
           { "name": "chainId", "type": "uint256" },
           { "name": "verifyingContract", "type": "address" }
       ],
       "Permit": [
           { "name": "holder", "type": "address" },
           { "name": "spender", "type": "address" },
           { "name": "nonce", "type": "uint256" },
           { "name": "allowed", "type": "bool" },
           { "name": "expiry", "type": "uint256" }
       ]
   },
   "primaryType": "Permit",
   "domain": {
       "name": "Dai Stablecoin",
       "version": "1",
       "chainId": 1, //Ethereum mainnet
       "verifyingContract": "0x6b175474e89094c44da98b954eedeac495271d0f"
   },
   "message": {
       "holder": "0x000000000000000000000000deadbeef",
       "spender": "0x00000000000000000000000000deface",
       "nonce": 123,
       "allowed": true
       "expiry": 12345678
   }
}

Where the data contained in message is the main information being signed. Signatures also include a hash of the domain data and the types required to parse the message.

One of the most common use cases for EIP-712 are Permits, like the one from the example above. Permits allow users to authorize actions in a smart contract without spending gas, and therefore delegate the execution of a transaction to another party (such as a relayer) by signing an EIP-712 message. Specifically, the EIP-2612 defines Permits for ERC-20 tokens.

The previous EIP-712-based Permit example grants the 0xdeface address unlimited allowance on the DAI balance of the address 0xdeadbeef, specifically within the contract 0x6b175474e89094c44da98b954eedeac495271d0f (DAI Ethereum mainnet).

The Vulnerability

A malicious DApp could inject arbitrary data in the message field of a EIP-712 message when calling the signTypedData or eth_signTypedDatav4 functions. This data is then displayed by the wallet as part of the signature preview, although it’s not used to compute the signature.

Let’s take a look at how the previous Permit example would look line if injected with fake data:

{
   "types": {
       "EIP712Domain": [
           { "name": "name", "type": "string" },
           { "name": "version", "type": "string" },
           { "name": "chainId", "type": "uint256" },
           { "name": "verifyingContract", "type": "address" }
       ],
       "Permit": [
           { "name": "holder", "type": "address" },
           { "name": "spender", "type": "address" },
           { "name": "nonce", "type": "uint256" },
           { "name": "allowed", "type": "bool" },
           { "name": "expiry", "type": "uint256" }
       ]
   },
   "primaryType": "Permit",
   "domain": {
       "name": "Dai Stablecoin",
       "version": "1",
       "chainId": 1, //Ethereum mainnet
       "verifyingContract": "0x6b175474e89094c44da98b954eedeac495271d0f"
   },
   "message": {
       "TX simulation": "NO RISK FOUND ✅ ⸻⸻⸻⸻⸻⸻⁢ ⁢ ⁢ ⁢ ⁢ ⁢ ⁢ 100 DAI will be transferred to your account",
       "holder": "0x000000000000000000000000deadbeef",
       "spender": "0x00000000000000000000000000deface",
       "nonce": 1,
       "allowed": true
       "expiry": 12345678
   }
}

Note the first -injected- field in the message object, whose key is TX simulation, and value is NO RISK FOUND ✅ ⸻⸻⸻⸻⸻⸻⁢ 100 DAI will be transferred to your account⁢. This message is rendered by the wallet as follows:

UI Injection Example

Ok, then what if we now add a bunch of whitespaces at the end of the injected data?

White spaces at the end

Looks pretty real right? Once the victim clicks on Confirm, the message is signed. However, because the injected field doesn’t correspond to any type in the types object, it will be discarded for the signature generation.

Finally, once the user signs the message, they grant the malicious DApp the required permissions to empty their DAI balance.