Skip to content

Commit 3dfdd04

Browse files
authored
Improve state size validation (#3025)
This PR contains a couple changes related to JSON size validation in the `snap_manageState` and `snap_setState` methods: - The maximum size was reduced from 100 MB to 64 MB. This is to because `window.postMessage` breaks when sending payloads that are too large. This is technically a breaking change, but I'm not sure if it should be considered as one, given that the execution environment would throw regardless, if the new state is too big? - `snap_setState` now properly validates the size of the new state, like `snap_manageState` does.
1 parent 687b582 commit 3dfdd04

File tree

5 files changed

+140
-25
lines changed

5 files changed

+140
-25
lines changed

packages/snaps-rpc-methods/jest.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ module.exports = deepmerge(baseConfig, {
1010
],
1111
coverageThreshold: {
1212
global: {
13-
branches: 93.97,
13+
branches: 94.89,
1414
functions: 98.05,
1515
lines: 98.67,
16-
statements: 98.25,
16+
statements: 98.34,
1717
},
1818
},
1919
});

packages/snaps-rpc-methods/src/permitted/setState.test.ts

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { JsonRpcEngine } from '@metamask/json-rpc-engine';
22
import { errorCodes } from '@metamask/rpc-errors';
33
import type { SetStateResult } from '@metamask/snaps-sdk';
4-
import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils';
4+
import type {
5+
Json,
6+
JsonRpcRequest,
7+
PendingJsonRpcResponse,
8+
} from '@metamask/utils';
59

610
import { setStateHandler, type SetStateParameters, set } from './setState';
711

@@ -396,6 +400,111 @@ describe('snap_setState', () => {
396400
},
397401
});
398402
});
403+
404+
it('throws if the new state is not JSON serialisable', async () => {
405+
const { implementation } = setStateHandler;
406+
407+
const getSnapState = jest.fn().mockReturnValue(null);
408+
const updateSnapState = jest.fn().mockReturnValue(null);
409+
const getUnlockPromise = jest.fn().mockResolvedValue(undefined);
410+
const hasPermission = jest.fn().mockReturnValue(true);
411+
412+
const hooks = {
413+
getSnapState,
414+
updateSnapState,
415+
getUnlockPromise,
416+
hasPermission,
417+
};
418+
419+
const engine = new JsonRpcEngine();
420+
421+
engine.push((request, response, next, end) => {
422+
const result = implementation(
423+
request as JsonRpcRequest<SetStateParameters>,
424+
response as PendingJsonRpcResponse<SetStateResult>,
425+
next,
426+
end,
427+
hooks,
428+
);
429+
430+
result?.catch(end);
431+
});
432+
433+
const response = await engine.handle({
434+
jsonrpc: '2.0',
435+
id: 1,
436+
method: 'snap_setState',
437+
params: {
438+
value: {
439+
// @ts-expect-error - BigInt is not JSON serializable.
440+
foo: 1n as Json,
441+
},
442+
},
443+
});
444+
445+
expect(response).toStrictEqual({
446+
jsonrpc: '2.0',
447+
id: 1,
448+
error: {
449+
code: errorCodes.rpc.invalidParams,
450+
message:
451+
'Invalid params: At path: value -- Expected a value of type `JSON`, but received: `[object Object]`.',
452+
stack: expect.any(String),
453+
},
454+
});
455+
});
456+
457+
it('throws if the new state exceeds the size limit', async () => {
458+
const { implementation } = setStateHandler;
459+
460+
const getSnapState = jest.fn().mockReturnValue(null);
461+
const updateSnapState = jest.fn().mockReturnValue(null);
462+
const getUnlockPromise = jest.fn().mockResolvedValue(undefined);
463+
const hasPermission = jest.fn().mockReturnValue(true);
464+
465+
const hooks = {
466+
getSnapState,
467+
updateSnapState,
468+
getUnlockPromise,
469+
hasPermission,
470+
};
471+
472+
const engine = new JsonRpcEngine();
473+
474+
engine.push((request, response, next, end) => {
475+
const result = implementation(
476+
request as JsonRpcRequest<SetStateParameters>,
477+
response as PendingJsonRpcResponse<SetStateResult>,
478+
next,
479+
end,
480+
hooks,
481+
);
482+
483+
result?.catch(end);
484+
});
485+
486+
const response = await engine.handle({
487+
jsonrpc: '2.0',
488+
id: 1,
489+
method: 'snap_setState',
490+
params: {
491+
value: {
492+
foo: 'foo'.repeat(21_500_000), // 64.5 MB
493+
},
494+
},
495+
});
496+
497+
expect(response).toStrictEqual({
498+
jsonrpc: '2.0',
499+
id: 1,
500+
error: {
501+
code: errorCodes.rpc.invalidParams,
502+
message:
503+
'Invalid params: The new state must not exceed 64 MB in size.',
504+
stack: expect.any(String),
505+
},
506+
});
507+
});
399508
});
400509
});
401510

packages/snaps-rpc-methods/src/permitted/setState.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,18 @@ import {
1313
} from '@metamask/superstruct';
1414
import type { PendingJsonRpcResponse, JsonRpcRequest } from '@metamask/utils';
1515
import {
16+
getJsonSize,
1617
hasProperty,
1718
isObject,
1819
assert,
1920
JsonStruct,
2021
type Json,
2122
} from '@metamask/utils';
2223

23-
import { manageStateBuilder } from '../restricted/manageState';
24+
import {
25+
manageStateBuilder,
26+
STORAGE_SIZE_LIMIT,
27+
} from '../restricted/manageState';
2428
import type { MethodHooksObject } from '../utils';
2529
import { FORBIDDEN_KEYS, StateKeyStruct } from '../utils';
2630

@@ -142,6 +146,16 @@ async function setStateImplementation(
142146
}
143147

144148
const newState = await getNewState(key, value, encrypted, getSnapState);
149+
150+
const size = getJsonSize(newState);
151+
if (size > STORAGE_SIZE_LIMIT) {
152+
throw rpcErrors.invalidParams({
153+
message: `Invalid params: The new state must not exceed ${
154+
STORAGE_SIZE_LIMIT / 1_000_000
155+
} MB in size.`,
156+
});
157+
}
158+
145159
await updateSnapState(newState, encrypted);
146160
response.result = null;
147161
} catch (error) {

packages/snaps-rpc-methods/src/restricted/manageState.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ describe('snap_manageState', () => {
333333
},
334334
}),
335335
).rejects.toThrow(
336-
'Invalid snap_manageState "updateState" parameter: The new state must be a plain object.',
336+
'Invalid snap_manageState "newState" parameter: The new state must be a plain object.',
337337
);
338338

339339
expect(updateSnapState).not.toHaveBeenCalledWith(
@@ -375,7 +375,7 @@ describe('snap_manageState', () => {
375375
},
376376
}),
377377
).rejects.toThrow(
378-
'Invalid snap_manageState "updateState" parameter: The new state must be JSON serializable.',
378+
'Invalid snap_manageState "newState" parameter: The new state must be JSON serializable.',
379379
);
380380

381381
expect(updateSnapState).not.toHaveBeenCalledWith(
@@ -459,7 +459,7 @@ describe('snap_manageState', () => {
459459
'snap_manageState',
460460
),
461461
).toThrow(
462-
'Invalid snap_manageState "updateState" parameter: The new state must be a plain object.',
462+
'Invalid snap_manageState "newState" parameter: The new state must be a plain object.',
463463
);
464464
});
465465

@@ -478,7 +478,7 @@ describe('snap_manageState', () => {
478478
'snap_manageState',
479479
),
480480
).toThrow(
481-
'Invalid snap_manageState "updateState" parameter: The new state must be JSON serializable.',
481+
'Invalid snap_manageState "newState" parameter: The new state must be JSON serializable.',
482482
);
483483
});
484484

@@ -498,7 +498,9 @@ describe('snap_manageState', () => {
498498
MOCK_SMALLER_STORAGE_SIZE_LIMIT,
499499
),
500500
).toThrow(
501-
`Invalid snap_manageState "updateState" parameter: The new state must not exceed ${MOCK_SMALLER_STORAGE_SIZE_LIMIT} bytes in size.`,
501+
`Invalid snap_manageState "newState" parameter: The new state must not exceed ${
502+
MOCK_SMALLER_STORAGE_SIZE_LIMIT / 1_000_000
503+
} MB in size.`,
502504
);
503505
});
504506
});

packages/snaps-rpc-methods/src/restricted/manageState.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export const manageStateBuilder = Object.freeze({
107107
methodHooks,
108108
} as const);
109109

110-
export const STORAGE_SIZE_LIMIT = 104857600; // In bytes (100MB)
110+
export const STORAGE_SIZE_LIMIT = 64_000_000; // In bytes (64 MB)
111111

112112
type GetEncryptionKeyArgs = {
113113
snapId: string;
@@ -256,11 +256,7 @@ export function getValidatedParams(
256256
if (operation === ManageStateOperation.UpdateState) {
257257
if (!isObject(newState)) {
258258
throw rpcErrors.invalidParams({
259-
message: `Invalid ${method} "updateState" parameter: The new state must be a plain object.`,
260-
data: {
261-
receivedNewState:
262-
typeof newState === 'undefined' ? 'undefined' : newState,
263-
},
259+
message: `Invalid ${method} "newState" parameter: The new state must be a plain object.`,
264260
});
265261
}
266262

@@ -270,21 +266,15 @@ export function getValidatedParams(
270266
size = getJsonSize(newState);
271267
} catch {
272268
throw rpcErrors.invalidParams({
273-
message: `Invalid ${method} "updateState" parameter: The new state must be JSON serializable.`,
274-
data: {
275-
receivedNewState:
276-
typeof newState === 'undefined' ? 'undefined' : newState,
277-
},
269+
message: `Invalid ${method} "newState" parameter: The new state must be JSON serializable.`,
278270
});
279271
}
280272

281273
if (size > storageSizeLimit) {
282274
throw rpcErrors.invalidParams({
283-
message: `Invalid ${method} "updateState" parameter: The new state must not exceed ${storageSizeLimit} bytes in size.`,
284-
data: {
285-
receivedNewState:
286-
typeof newState === 'undefined' ? 'undefined' : newState,
287-
},
275+
message: `Invalid ${method} "newState" parameter: The new state must not exceed ${
276+
storageSizeLimit / 1_000_000
277+
} MB in size.`,
288278
});
289279
}
290280
}

0 commit comments

Comments
 (0)