-
-
Notifications
You must be signed in to change notification settings - Fork 294
feat: validate eth_sendTransaction / eth_signTransaction params
#9482
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a24978d
0c9d578
8371c84
6307196
35cfbb3
969b628
25a792e
d874145
2b7e306
c859134
61aeef4
eae0c21
157e36e
82e84ca
0dce32f
0a9c9a7
acd7ff7
12c265a
df3959f
740f112
593ce7e
81658c6
a4e603a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,15 @@ | ||
| import { TYPED_MESSAGE_SCHEMA } from '@metamask/eth-sig-util'; | ||
| import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; | ||
| import type { Struct, StructError } from '@metamask/superstruct'; | ||
| import { validate } from '@metamask/superstruct'; | ||
| import { | ||
| array, | ||
| number, | ||
| object, | ||
| optional, | ||
| string, | ||
| union, | ||
| validate, | ||
| } from '@metamask/superstruct'; | ||
| import type { Hex } from '@metamask/utils'; | ||
|
|
||
| import type { WalletMiddlewareContext } from '../wallet.js'; | ||
|
|
@@ -234,3 +242,78 @@ export function validateTypedMessageKeys(data: string): void { | |
| } | ||
| } | ||
| } | ||
|
|
||
| // Numerical fields accept both hex strings and numbers, as some dapps send | ||
| // numbers and `TransactionController` normalizes them downstream. | ||
| const QuantityStruct = union([string(), number()]); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❤️ |
||
|
|
||
| export const TransactionParamsStruct = object({ | ||
| accessList: optional( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm sure we'll hit issues with some dApps as we can't predict all inputs, but we're long overdue schema validation here, so this should be a safe start 👍 |
||
| array(object({ address: string(), storageKeys: array(string()) })), | ||
| ), | ||
| authorizationList: optional( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor, we don't allow external authorization lists for security, but that will be caught in |
||
| array( | ||
| object({ | ||
| address: string(), | ||
| chainId: optional(string()), | ||
| nonce: optional(string()), | ||
| r: optional(string()), | ||
| s: optional(string()), | ||
| yParity: optional(string()), | ||
| }), | ||
| ), | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| ), | ||
| chainId: optional(string()), | ||
| data: optional(string()), | ||
| from: string(), | ||
| gas: optional(QuantityStruct), | ||
| gasLimit: optional(QuantityStruct), | ||
| gasPrice: optional(QuantityStruct), | ||
| maxFeePerGas: optional(QuantityStruct), | ||
| maxPriorityFeePerGas: optional(QuantityStruct), | ||
| nonce: optional(QuantityStruct), | ||
| to: optional(string()), | ||
| type: optional(string()), | ||
| value: optional(QuantityStruct), | ||
| }); | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| // Upper bound derived from the largest valid eth_sendTransaction payload: | ||
| // EIP-3860 caps initcode at 49,152 bytes → hex-encoded in 'data' field ≈ 98 KB of JSON. | ||
| // 200 KB is ~2× that ceiling, giving clear headroom above any protocol-legal | ||
| // transaction while blocking the padding attacks this cap defends against. | ||
| // TODO(CONF-1662): tighten once P99 production data is available. | ||
| export const MAX_TRANSACTION_PARAMS_SIZE_BYTES = 200 * 1024; | ||
|
|
||
| /** | ||
| * Validates `eth_sendTransaction` / `eth_signTransaction` params against the | ||
| * standard transaction schema and rejects payloads whose serialized size | ||
| * exceeds `MAX_TRANSACTION_PARAMS_SIZE_BYTES`. | ||
| * | ||
| * Guards against two attack shapes: | ||
| * - Size: valid-shaped but oversized payloads (e.g. `data` padded with | ||
| * millions of hex zeros) that exhaust memory in downstream code. Checked | ||
| * first via `JSON.stringify` so oversized input is rejected before schema | ||
| * work. | ||
| * - Structural: extraneous top-level keys or ill-typed fields (e.g. | ||
| * `{ from, to, test: { b: { b: ... × 1200 } } }`) that would crash | ||
| * downstream normalization / PPOM WASM with `RangeError: Maximum call | ||
| * stack size exceeded`, silently bypassing security checks. Superstruct's | ||
| * `object()` rejects unknown keys by name without accessing their values, | ||
| * so hostile nested subtrees are never traversed by schema validation. | ||
| * | ||
| * @param params - The transaction params object supplied by the dapp. | ||
| * @throws rpcErrors.invalidParams() if params is an array or exceeds the | ||
| * serialized size limit. | ||
| * @throws rpcErrors.invalidInput() if params fails schema validation | ||
| * (wrong type, extraneous top-level key, or malformed nested field). | ||
| */ | ||
| export function validateTransactionParams(params: unknown): void { | ||
| if ( | ||
| new TextEncoder().encode(JSON.stringify(params)).byteLength > | ||
| MAX_TRANSACTION_PARAMS_SIZE_BYTES | ||
| ) { | ||
| throw rpcErrors.invalidParams('Request too large'); | ||
| } | ||
|
|
||
| validateParams(params, TransactionParamsStruct); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.