diff --git a/config/cspell-ts.json b/config/cspell-ts.json index abda1bfccda..bae5c61b981 100644 --- a/config/cspell-ts.json +++ b/config/cspell-ts.json @@ -24,6 +24,7 @@ } ], "words": [ + "MAXPAYLOAD", "immediates", "unerasable", "bytelist", diff --git a/packages/client/bin/repl.ts b/packages/client/bin/repl.ts index 3122920f908..4b7b59d4bd6 100644 --- a/packages/client/bin/repl.ts +++ b/packages/client/bin/repl.ts @@ -7,7 +7,7 @@ import { startRPCServers } from './startRPC.ts' import { generateClientConfig, getArgs } from './utils.ts' import type { Common, GenesisState } from '@ethereumjs/common' -import type { Config } from '../src/config.ts' +import { Config } from '../src/config.ts' import type { EthereumClient } from '../src/index.ts' import type { ClientOpts } from '../src/types.ts' @@ -42,6 +42,8 @@ const setupClient = async ( jwtSecret: '', rpcEngineAuth: false, rpcCors: '', + rpcEthMaxPayload: args.rpcEthMaxPayload ?? Config.RPC_ETH_MAXPAYLOAD_DEFAULT, + rpcEngineMaxPayload: args.rpcEngineMaxPayload ?? Config.RPC_ENGINE_MAXPAYLOAD_DEFAULT, }) return { client, executionRPC: servers[0], engineRPC: servers[1] } diff --git a/packages/client/bin/startRPC.ts b/packages/client/bin/startRPC.ts index 8e6e05f587a..0126640b206 100644 --- a/packages/client/bin/startRPC.ts +++ b/packages/client/bin/startRPC.ts @@ -37,6 +37,8 @@ export type RPCArgs = { jwtSecret?: string rpcEngineAuth: boolean rpcCors: string + rpcEthMaxPayload: string + rpcEngineMaxPayload: string } /** @@ -97,7 +99,10 @@ export function startRPCServers(client: EthereumClient, args: RPCArgs) { rpcCors, rpcDebug, rpcDebugVerbose, + rpcEthMaxPayload, + rpcEngineMaxPayload, } = args + const manager = new RPCManager(client, config) const { logger } = config const jwtSecret = @@ -136,6 +141,7 @@ export function startRPCServers(client: EthereumClient, args: RPCArgs) { : req.body.method.includes('engine_') === false, } : undefined, + maxPayload: rpcEthMaxPayload, }) rpcHttpServer.listen(rpcPort, rpcAddr) logger?.info( @@ -191,6 +197,7 @@ export function startRPCServers(client: EthereumClient, args: RPCArgs) { jwtSecret, } : undefined, + maxPayload: rpcEngineMaxPayload, }) rpcHttpServer.listen(rpcEnginePort, rpcEngineAddr) logger?.info( diff --git a/packages/client/bin/utils.ts b/packages/client/bin/utils.ts index 2c034967fad..9709af289b8 100644 --- a/packages/client/bin/utils.ts +++ b/packages/client/bin/utils.ts @@ -196,6 +196,16 @@ export function getArgs(): ClientOpts { boolean: true, default: true, }) + .option('rpcEthMaxPayload', { + describe: 'Define max JSON payload size for eth/debug RPC requests', + string: true, + default: Config.RPC_ETH_MAXPAYLOAD_DEFAULT, + }) + .option('rpcEngineMaxPayload', { + describe: 'Define max JSON payload size for engine RPC requests', + string: true, + default: Config.RPC_ENGINE_MAXPAYLOAD_DEFAULT, + }) .option('jwtSecret', { describe: 'Provide a file containing a hex encoded jwt secret for Engine RPC server', coerce: (arg: string) => (arg ? path.resolve(arg) : undefined), diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts index a99dbfe9802..9ace55de5f0 100644 --- a/packages/client/src/config.ts +++ b/packages/client/src/config.ts @@ -345,6 +345,16 @@ export interface ConfigOptions { * Enables Prometheus Metrics that can be collected for monitoring client health */ prometheusMetrics?: PrometheusMetrics + + /** + * Max JSON payload size for eth/eth RPC requests (untrusted) + */ + rpcEthMaxPayload?: string + + /* + * Max JSON payload size for engine RPC requests (trusted) + */ + rpcEngineMaxPayload?: string } export class Config { @@ -364,6 +374,8 @@ export class Config { public static readonly MINPEERS_DEFAULT = 1 public static readonly MAXPEERS_DEFAULT = 25 public static readonly DNSADDR_DEFAULT = '8.8.8.8' + public static readonly RPC_ETH_MAXPAYLOAD_DEFAULT = '11mb' + public static readonly RPC_ENGINE_MAXPAYLOAD_DEFAULT = '15mb' public static readonly EXECUTION = true public static readonly NUM_BLOCKS_PER_ITERATION = 100 public static readonly ACCOUNT_CACHE = 400000 @@ -457,6 +469,9 @@ export class Config { public readonly blobsAndProofsCacheBlocks: number + public readonly rpcEthMaxPayload: string + public readonly rpcEngineMaxPayload: string + public synchronized: boolean public lastSynchronized?: boolean /** lastSyncDate in ms */ @@ -562,6 +577,9 @@ export class Config { this.blobsAndProofsCacheBlocks = options.blobsAndProofsCacheBlocks ?? Config.BLOBS_AND_PROOFS_CACHE_BLOCKS + this.rpcEthMaxPayload = options.rpcEthMaxPayload ?? Config.RPC_ETH_MAXPAYLOAD_DEFAULT + this.rpcEngineMaxPayload = options.rpcEngineMaxPayload ?? Config.RPC_ENGINE_MAXPAYLOAD_DEFAULT + this.discDns = this.getDnsDiscovery(options.discDns) this.discV4 = options.discV4 ?? true diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index fa13e3f595a..8d97436ca50 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -148,6 +148,8 @@ export interface ClientOpts { skipEngineExec?: boolean ignoreStatelessInvalidExecs?: boolean useJsCrypto?: boolean + rpcEthMaxPayload?: string + rpcEngineMaxPayload?: string } export type PrometheusMetrics = { diff --git a/packages/client/src/util/rpc.ts b/packages/client/src/util/rpc.ts index 3ebf507622f..8813743e7fd 100644 --- a/packages/client/src/util/rpc.ts +++ b/packages/client/src/util/rpc.ts @@ -33,6 +33,7 @@ type CreateRPCServerListenerOpts = { withEngineMiddleware?: WithEngineMiddleware } type CreateWSServerOpts = CreateRPCServerListenerOpts & { httpServer?: jayson.HttpServer } +type CreateHTTPServerOpts = CreateRPCServerListenerOpts & { maxPayload?: string } type WithEngineMiddleware = { jwtSecret: Uint8Array; unlessFn?: (req: IncomingMessage) => boolean } export type MethodConfig = (typeof MethodConfig)[keyof typeof MethodConfig] @@ -182,13 +183,13 @@ function checkHeaderAuth(req: any, jwtSecret: Uint8Array): void { } } -export function createRPCServerListener(opts: CreateRPCServerListenerOpts): jayson.HttpServer { - const { server, withEngineMiddleware, RPCCors } = opts +export function createRPCServerListener(opts: CreateHTTPServerOpts): jayson.HttpServer { + const { server, withEngineMiddleware, RPCCors, maxPayload } = opts const app = Connect() if (typeof RPCCors === 'string') app.use(cors({ origin: RPCCors })) // GOSSIP_MAX_SIZE_BELLATRIX is proposed to be 10MiB - app.use(JSONParser({ limit: '11mb' })) + app.use(JSONParser({ limit: maxPayload })) if (withEngineMiddleware) { const { jwtSecret, unlessFn } = withEngineMiddleware diff --git a/packages/client/test/util/rpc.spec.ts b/packages/client/test/util/rpc.spec.ts index 1f207b49395..fe7e4f5a122 100644 --- a/packages/client/test/util/rpc.spec.ts +++ b/packages/client/test/util/rpc.spec.ts @@ -32,6 +32,7 @@ describe('[Util/RPC]', () => { const httpServer = createRPCServerListener({ server, withEngineMiddleware: { jwtSecret: new Uint8Array(32) }, + maxPayload: Config.RPC_ETH_MAXPAYLOAD_DEFAULT, }) const wsServer = createWsRPCServerListener({ server, @@ -74,6 +75,7 @@ describe('[Util/RPC]', () => { const httpServer = createRPCServerListener({ server, withEngineMiddleware: { jwtSecret: new Uint8Array(32) }, + maxPayload: Config.RPC_ENGINE_MAXPAYLOAD_DEFAULT, }) const wsServer = createWsRPCServerListener({ server, @@ -84,6 +86,96 @@ describe('[Util/RPC]', () => { 'should return http and ws servers', ) }) + it('should reject oversized RPC payloads', async () => { + const config = new Config({ + accountCache: 10000, + storageCache: 1000, + rpcEthMaxPayload: '1kb', + rpcEngineMaxPayload: '10mb', + }) + const client = await EthereumClient.create({ config, metaDB: new MemoryLevel() }) + + const manager = new RPCManager(client, config) + const { logger } = config + const methodConfig = Object.values(MethodConfig)[0] + const { server } = createRPCServer(manager, { + methodConfig, + rpcDebug: 'eth', + logger, + rpcDebugVerbose: undefined as any, + }) + + const ethHttpServer = createRPCServerListener({ + server, + withEngineMiddleware: undefined, + maxPayload: config.rpcEthMaxPayload, + }) + + const engineHttpServer = createRPCServerListener({ + server, + withEngineMiddleware: undefined, + maxPayload: config.rpcEngineMaxPayload, + }) + + const ethPort = 8545 + const enginePort = 8551 + + ethHttpServer.listen(ethPort) + engineHttpServer.listen(enginePort) + + const oversizedEthPayload = JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'eth_getBlockByNumber', + params: ['latest', true], + data: 'eth'.repeat(2500), + }) + + const oversizedEnginePayload = JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'engine_newPayloadV2', + params: [ + { + baseFeePerGas: '0x', + blockHash: '0x', + blockNumber: '0x', + extraData: '0x'.repeat(2500), + feeRecipient: '0x', + gasLimit: '0x', + gasUsed: '0x', + logsBloom: '0x', + parentHash: '0x', + prevRandao: '0x', + receiptsRoot: '0x', + stateRoot: '0x', + timestamp: '0x', + transactions: [], + }, + ], + }) + + const resEth = await fetch(`http://localhost:${ethPort}`, { + method: 'POST', + body: oversizedEthPayload, + headers: { 'Content-Type': 'application/json' }, + }) + + const resEngine = await fetch(`http://localhost:${enginePort}`, { + method: 'POST', + body: oversizedEnginePayload, + headers: { + 'Content-Type': 'application/json', + }, + }) + + assert.strictEqual( + resEth.status, + 413, + 'ETH server should reject oversized payload with 413 status', + ) + assert.strictEqual(resEngine.status, 200, 'ENGINE server should accept oversized payload') + }) }) describe('[Util/RPC/Engine eth methods]', async () => {