@@ -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+
270274export 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 */
292312export 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
0 commit comments