diff --git a/packages/eth-json-rpc-middleware/CHANGELOG.md b/packages/eth-json-rpc-middleware/CHANGELOG.md index 86c71ec6dce..62d436f4187 100644 --- a/packages/eth-json-rpc-middleware/CHANGELOG.md +++ b/packages/eth-json-rpc-middleware/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Add more strict validation for signTypedData V4 requests ([#8526](https://github.com/MetaMask/core/pull/8526)) + ## [23.1.1] ### Changed diff --git a/packages/eth-json-rpc-middleware/src/utils/validation.test.ts b/packages/eth-json-rpc-middleware/src/utils/validation.test.ts index 8191436037b..0b572b9a31d 100644 --- a/packages/eth-json-rpc-middleware/src/utils/validation.test.ts +++ b/packages/eth-json-rpc-middleware/src/utils/validation.test.ts @@ -8,6 +8,7 @@ import { resemblesAddress, validateAndNormalizeKeyholder, validateParams, + validateTypedMessageKeys, } from './validation'; jest.mock('@metamask/superstruct', () => ({ @@ -125,4 +126,39 @@ describe('Validation Utils', () => { `); }); }); + + describe('validateTypedMessageKeys', () => { + it('does not throw for data with only schema-defined keys', () => { + const data = JSON.stringify({ + types: { + EIP712Domain: [{ name: 'name', type: 'string' }], + }, + }); + + expect(() => validateTypedMessageKeys(data)).not.toThrow(); + }); + + it('throws for data with extraneous keys', () => { + const data = JSON.stringify({ + types: { + EIP712Domain: [{ name: 'name', type: 'string' }], + }, + primaryType: 'EIP712Domain', + domain: {}, + message: {}, + extraKey: 'unexpected', + }); + + expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.'); + }); + + it('throws when data contains only extraneous keys', () => { + const data = JSON.stringify({ + foo: 'bar', + baz: 123, + }); + + expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.'); + }); + }); }); diff --git a/packages/eth-json-rpc-middleware/src/utils/validation.ts b/packages/eth-json-rpc-middleware/src/utils/validation.ts index 7d291aec56c..f65a6bc41ba 100644 --- a/packages/eth-json-rpc-middleware/src/utils/validation.ts +++ b/packages/eth-json-rpc-middleware/src/utils/validation.ts @@ -1,3 +1,4 @@ +import { TYPED_MESSAGE_SCHEMA } from '@metamask/eth-sig-util'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { Struct, StructError } from '@metamask/superstruct'; import { validate } from '@metamask/superstruct'; @@ -187,3 +188,23 @@ export function validateTypedDataForPrototypePollution(data: string): void { checkObjectForPrototypePollution(message); } } + +/** + * Validates that EIP-712 typed message data contains only keys defined in + * the TYPED_MESSAGE_SCHEMA from `@metamask/eth-sig-util`. Rejects messages + * with extraneous top-level keys. + * + * @param data - The stringified typed data to validate. + * @throws rpcErrors.invalidInput() if extraneous keys are detected. + */ +export function validateTypedMessageKeys(data: string): void { + const parsedData = parseTypedMessage(data); + const allowedKeys = new Set(Object.keys(TYPED_MESSAGE_SCHEMA.properties)); + const hasExtraneousKey = Object.keys(parsedData).some( + (key) => !allowedKeys.has(key), + ); + + if (hasExtraneousKey) { + throw rpcErrors.invalidInput(); + } +} diff --git a/packages/eth-json-rpc-middleware/src/wallet.test.ts b/packages/eth-json-rpc-middleware/src/wallet.test.ts index 3c8d69fd36e..a9964c0d3a1 100644 --- a/packages/eth-json-rpc-middleware/src/wallet.test.ts +++ b/packages/eth-json-rpc-middleware/src/wallet.test.ts @@ -757,6 +757,29 @@ describe('wallet', () => { engine.handle(...createHandleParams(payload)), ).rejects.toThrow('Invalid input.'); }); + + it('should throw if message data contains extraneous keys', async () => { + const getAccounts = async (): Promise => testAddresses.slice(); + const processTypedMessageV4 = async (): Promise => testMsgSig; + const engine = JsonRpcEngineV2.create({ + middleware: [ + createWalletMiddleware({ getAccounts, processTypedMessageV4 }), + ], + }); + + const messageParams = getMsgParams(); + const payload = { + method: 'eth_signTypedData_v4', + params: [ + testAddresses[0], + JSON.stringify({ ...messageParams, extraKey: 'unexpected' }), + ], + }; + + await expect( + engine.handle(...createHandleParams(payload)), + ).rejects.toThrow('Invalid input.'); + }); }); describe('sign', () => { diff --git a/packages/eth-json-rpc-middleware/src/wallet.ts b/packages/eth-json-rpc-middleware/src/wallet.ts index 24394190640..b220be16e09 100644 --- a/packages/eth-json-rpc-middleware/src/wallet.ts +++ b/packages/eth-json-rpc-middleware/src/wallet.ts @@ -25,6 +25,7 @@ import { validateAndNormalizeKeyholder as validateKeyholder, validateTypedDataForPrototypePollution, validateTypedDataV1ForPrototypePollution, + validateTypedMessageKeys, } from './utils/validation'; export type TransactionParams = { @@ -408,6 +409,7 @@ export function createWalletMiddleware({ const address = await validateAndNormalizeKeyholder(params[0], context); const message = normalizeTypedMessage(params[1]); + validateTypedMessageKeys(message); validatePrimaryType(message); validateVerifyingContract(message); validateTypedDataForPrototypePollution(message);