Skip to content

Commit c8ec918

Browse files
committed
Update size
1 parent 7114e30 commit c8ec918

3 files changed

Lines changed: 77 additions & 18 deletions

File tree

packages/eth-json-rpc-middleware/src/utils/validation.test.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ describe('Validation Utils', () => {
350350
to: VALID_TO,
351351
extraKey: 'unexpected',
352352
}),
353-
).toThrow(/Invalid params/u);
353+
).toThrow('Invalid input.');
354354
});
355355

356356
it('throws for the incident repro payload (deeply-nested junk field)', () => {
@@ -367,7 +367,37 @@ describe('Validation Utils', () => {
367367
data: '0x095ea7b3',
368368
test: junk,
369369
}),
370-
).toThrow(/Invalid params|Invalid input/u);
370+
).toThrow('Invalid input.');
371+
});
372+
373+
it('rejects an extraneous top-level key without walking its value (no JSON.stringify, no Superstruct)', () => {
374+
const stringifySpy = jest.spyOn(JSON, 'stringify');
375+
validateMock.mockClear();
376+
377+
const params = {
378+
from: VALID_FROM,
379+
to: VALID_TO,
380+
test: {
381+
get b(): never {
382+
throw new Error('subtree must not be walked');
383+
},
384+
},
385+
};
386+
387+
let thrown: unknown;
388+
try {
389+
validateTransactionParams(params);
390+
} catch (error) {
391+
thrown = error;
392+
}
393+
const stringifyCallsAtRejection = stringifySpy.mock.calls.length;
394+
const validateCallsAtRejection = validateMock.mock.calls.length;
395+
stringifySpy.mockRestore();
396+
397+
expect(thrown).toBeInstanceOf(Error);
398+
expect((thrown as Error).message).toBe('Invalid input.');
399+
expect(stringifyCallsAtRejection).toBe(0);
400+
expect(validateCallsAtRejection).toBe(0);
371401
});
372402

373403
it('throws when a typed field has the wrong type', () => {

packages/eth-json-rpc-middleware/src/utils/validation.ts

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -267,31 +267,60 @@ export const TransactionParamsStruct = object({
267267
value: optional(StrictHexStruct),
268268
});
269269

270+
const ALLOWED_TRANSACTION_PARAM_KEYS = new Set<string>(
271+
Object.keys(TransactionParamsStruct.schema as Record<string, unknown>),
272+
);
273+
270274
export const MAX_TRANSACTION_PARAMS_SIZE_BYTES = 128 * 1024;
271275

272276
/**
273277
* Validates `eth_sendTransaction` / `eth_signTransaction` params against the
274278
* standard transaction schema and rejects payloads whose serialized size
275279
* exceeds `MAX_TRANSACTION_PARAMS_SIZE_BYTES`.
276280
*
277-
* Guards against two attack shapes:
278-
* 1. Structural: extraneous top-level keys or ill-typed nested values
279-
* (e.g. `{ from, to, data, test: { b: { b: ... } } }`) that would crash
280-
* downstream normalization / PPOM WASM with `RangeError: Maximum call
281-
* stack size exceeded`, silently bypassing security checks.
282-
* 2. Size: valid-shaped but oversized payloads (e.g. `data` padded with
283-
* millions of hex zeros, or `accessList` with millions of entries) that
284-
* exhaust memory / stack in the same downstream code.
281+
* Checks run in this order to guarantee we never recurse into hostile
282+
* subtrees:
283+
*
284+
* 1. Top-level shape: params must be a plain object whose top-level keys
285+
* are all in the schema. Runs in O(top-level-keys) without visiting
286+
* nested values, so a deeply-nested subtree under an extraneous key
287+
* (e.g. `{ from, to, test: { b: { b: ... × 1200 } } }`) is rejected
288+
* before any recursive walk can `RangeError`.
289+
* 2. Serialized size: `JSON.stringify(params).length` must be
290+
* `<= MAX_TRANSACTION_PARAMS_SIZE_BYTES`. Safe to walk at this point
291+
* because step 1 guarantees the only nested values live under
292+
* schema-declared fields (`accessList`, `authorizationList`) which are
293+
* shallow arrays of flat objects.
294+
* 3. Full schema validation: per-field types (hex strings, addresses,
295+
* `accessList` / `authorizationList` entry shapes).
296+
*
297+
* Together these guard against:
298+
* - Structural attacks: extraneous top-level keys or ill-typed nested
299+
* values that would crash downstream normalization / PPOM WASM with
300+
* `RangeError: Maximum call stack size exceeded`, silently bypassing
301+
* security checks.
302+
* - Size attacks: valid-shaped but oversized payloads (e.g. `data` padded
303+
* with millions of hex zeros) that exhaust memory / stack in the same
304+
* downstream code.
285305
*
286306
* @param params - The transaction params object supplied by the dapp.
287-
* @throws rpcErrors.invalidInput() if params does not match the schema or
288-
* exceeds the size limit.
307+
* @throws rpcErrors.invalidInput() if params is not a plain object,
308+
* contains an extraneous top-level key, or exceeds the size limit.
289309
* @throws rpcErrors.invalidParams() with a Superstruct failure summary if
290310
* the schema mismatch is on a typed field.
291311
*/
292312
export function validateTransactionParams(params: unknown): void {
293-
const serializedSize = JSON.stringify(params ?? null).length;
294-
if (serializedSize > MAX_TRANSACTION_PARAMS_SIZE_BYTES) {
313+
if (params === null || typeof params !== 'object' || Array.isArray(params)) {
314+
throw rpcErrors.invalidInput();
315+
}
316+
317+
for (const key of Object.keys(params)) {
318+
if (!ALLOWED_TRANSACTION_PARAM_KEYS.has(key)) {
319+
throw rpcErrors.invalidInput();
320+
}
321+
}
322+
323+
if (JSON.stringify(params).length > MAX_TRANSACTION_PARAMS_SIZE_BYTES) {
295324
throw rpcErrors.invalidInput();
296325
}
297326

packages/eth-json-rpc-middleware/src/wallet.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ describe('wallet', () => {
171171

172172
await expect(
173173
engine.handle(...createHandleParams(payload)),
174-
).rejects.toThrow(/Invalid params/u);
174+
).rejects.toThrow('Invalid input.');
175175
});
176176

177177
it('throws for the incident repro payload with deeply-nested junk', async () => {
@@ -204,7 +204,7 @@ describe('wallet', () => {
204204

205205
await expect(
206206
engine.handle(...createHandleParams(payload)),
207-
).rejects.toThrow(/Invalid params|Invalid input/u);
207+
).rejects.toThrow('Invalid input.');
208208
});
209209

210210
it('should not override other request params', async () => {
@@ -365,7 +365,7 @@ describe('wallet', () => {
365365

366366
await expect(
367367
engine.handle(...createHandleParams(payload)),
368-
).rejects.toThrow(/Invalid params/u);
368+
).rejects.toThrow('Invalid input.');
369369
});
370370

371371
it('throws for the incident repro payload with deeply-nested junk', async () => {
@@ -398,7 +398,7 @@ describe('wallet', () => {
398398

399399
await expect(
400400
engine.handle(...createHandleParams(payload)),
401-
).rejects.toThrow(/Invalid params|Invalid input/u);
401+
).rejects.toThrow('Invalid input.');
402402
});
403403
});
404404

0 commit comments

Comments
 (0)