From 17f9432de8502ce6b04b01ab63d0828759f62367 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Tue, 28 Apr 2026 18:44:38 +1200 Subject: [PATCH] Allow Advanced Permissions metadata in signtypeddata payload --- packages/eth-json-rpc-middleware/CHANGELOG.md | 4 + .../src/utils/validation.test.ts | 117 ++++++++++++++++++ .../src/utils/validation.ts | 28 ++++- 3 files changed, 148 insertions(+), 1 deletion(-) diff --git a/packages/eth-json-rpc-middleware/CHANGELOG.md b/packages/eth-json-rpc-middleware/CHANGELOG.md index ed1da74e8cd..5f0f7ddd07d 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] +### Fixed + +- Allow Advanced Permissions `metadata` in signTypedData V4 requests ([#8603](https://github.com/MetaMask/core/pull/8603)) + ## [23.1.2] ### 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 0b572b9a31d..230b476a4cf 100644 --- a/packages/eth-json-rpc-middleware/src/utils/validation.test.ts +++ b/packages/eth-json-rpc-middleware/src/utils/validation.test.ts @@ -160,5 +160,122 @@ describe('Validation Utils', () => { expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.'); }); + + describe('metadata', () => { + const baseTypedData = { + types: { + EIP712Domain: [{ name: 'name', type: 'string' }], + }, + primaryType: 'EIP712Domain', + domain: {}, + message: {}, + }; + + it('does not throw when metadata has exactly justification and origin as strings', () => { + const data = JSON.stringify({ + ...baseTypedData, + metadata: { + justification: 'Permission to spend tokens', + origin: 'https://example.com', + }, + }); + + expect(() => validateTypedMessageKeys(data)).not.toThrow(); + }); + + it('does not throw when metadata is the only top-level key', () => { + const data = JSON.stringify({ + metadata: { + justification: 'Permission to spend tokens', + origin: 'https://example.com', + }, + }); + + expect(() => validateTypedMessageKeys(data)).not.toThrow(); + }); + + it.each([ + ['null', null], + ['a string', 'not-an-object'], + ['a number', 42], + ['a boolean', true], + ['an array', ['justification', 'origin']], + ])('throws when metadata is %s', (_label, value) => { + const data = JSON.stringify({ + ...baseTypedData, + metadata: value, + }); + + expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.'); + }); + + it('throws when metadata is missing justification', () => { + const data = JSON.stringify({ + ...baseTypedData, + metadata: { + origin: 'https://example.com', + }, + }); + + expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.'); + }); + + it('throws when metadata is missing origin', () => { + const data = JSON.stringify({ + ...baseTypedData, + metadata: { + justification: 'Permission to spend tokens', + }, + }); + + expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.'); + }); + + it('throws when metadata.justification is not a string', () => { + const data = JSON.stringify({ + ...baseTypedData, + metadata: { + justification: 123, + origin: 'https://example.com', + }, + }); + + expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.'); + }); + + it('throws when metadata.origin is not a string', () => { + const data = JSON.stringify({ + ...baseTypedData, + metadata: { + justification: 'Permission to spend tokens', + origin: 123, + }, + }); + + expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.'); + }); + + it('throws when metadata has an extraneous third key', () => { + const data = JSON.stringify({ + ...baseTypedData, + metadata: { + justification: 'Permission to spend tokens', + origin: 'https://example.com', + extra: 'unexpected', + }, + }); + + expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.'); + }); + + it('throws when metadata is an empty object', () => { + const data = JSON.stringify({ + ...baseTypedData, + metadata: {}, + }); + + 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 f65a6bc41ba..de3782fa67b 100644 --- a/packages/eth-json-rpc-middleware/src/utils/validation.ts +++ b/packages/eth-json-rpc-middleware/src/utils/validation.ts @@ -199,7 +199,10 @@ export function validateTypedDataForPrototypePollution(data: string): void { */ export function validateTypedMessageKeys(data: string): void { const parsedData = parseTypedMessage(data); - const allowedKeys = new Set(Object.keys(TYPED_MESSAGE_SCHEMA.properties)); + const allowedKeys = new Set([ + ...Object.keys(TYPED_MESSAGE_SCHEMA.properties), + 'metadata', + ]); const hasExtraneousKey = Object.keys(parsedData).some( (key) => !allowedKeys.has(key), ); @@ -207,4 +210,27 @@ export function validateTypedMessageKeys(data: string): void { if (hasExtraneousKey) { throw rpcErrors.invalidInput(); } + + // Advanced Permissions adds `metadata: { justification: string, origin: string }` to eth_signTypedData requests. + // see GatorPermissionsController.decodePermissionFromPermissionContextForOrigin for more details. + const { metadata } = parsedData as { metadata?: unknown }; + if (metadata !== undefined) { + if (typeof metadata !== 'object' || metadata === null) { + throw rpcErrors.invalidInput(); + } + + const { justification, origin } = metadata as { + justification?: unknown; + origin?: unknown; + }; + + if (typeof justification !== 'string' || typeof origin !== 'string') { + throw rpcErrors.invalidInput(); + } + + // we only need to check the keys length, because we already checked the known keys (justification and origin). + if (Object.keys(metadata).length !== 2) { + throw rpcErrors.invalidInput(); + } + } }