Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/eth-json-rpc-middleware/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
117 changes: 117 additions & 0 deletions packages/eth-json-rpc-middleware/src/utils/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
});
});
});
});
28 changes: 27 additions & 1 deletion packages/eth-json-rpc-middleware/src/utils/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,38 @@ 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),
);

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();
}
}
}
Loading