A UI Flaw in Top Crypto Wallets We Need to Address
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.
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 Wallets
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.
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.
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:
Ok, then what if we now add a bunch of whitespaces at the end of the injected data?
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.