How a wide-range eth_getLogs query turns a few hundred bytes into an unbounded response on exposed go-ethereum RPC nodes
eth_getLogs returns every log matching a {fromBlock, toBlock, address, topics} filter across the requested block span, serialised into a single JSON-RPC response — so a few hundred bytes of request expands into an arbitrarily large reply bounded only by how many logs match, a response-amplification denial-of-service payable by an unauthenticated caller when go-ethereum's JSON-RPC transport is bound to a routable interface.
Why this matters
Anyone running a go-ethereum node with the HTTP or WebSocket JSON-RPC exposed — public and semi-public RPC providers, indexers, and self-hosted endpoints — serves eth_getLogs as an unauthenticated method. It is a documented expensive read, and go-ethereum ships a control (--rpc.rangelimit) for it, but that control bounds the wrong dimension. It caps the block span an attacker may request; it does not cap the number or byte-size of logs a permitted span returns. A naïve fix — "just set a range limit" — therefore does not close the amplification: a single query that stays inside the configured range but selects a high-volume topic (for example a busy ERC-20 Transfer event) still forces the node to scan the range and emit the whole matched-log set. The cost lands as egress bandwidth plus serialisation CPU on the node, per request, from an unauthenticated source.
How the attack works
- Entry point: the JSON-RPC
eth_getLogsmethod on the HTTP (--http) or WebSocket (--ws) transport. go-ethereum applies no per-method authorization on these transports; reachability is purely a function of the bind address and the enabled namespaces. - Preconditions: the log-filter RPC is reachable on a non-loopback address, with
--rpc.rangelimitunset or set loose and no result-size/result-count cap at the RPC edge. The loopback default is not remotely reachable. - Processing path:
eth_getLogsis served byFilterAPI.GetLogs(eth/filters/api.go), which builds aFilterand callsfilter.Logs(ctx)(eth/filters/filter.go).Logsresolves the[fromBlock, toBlock]range and walks it via an indexed and an unindexed path; for every block whose log bloom matches, it decodes the receipts and appends every matching*types.Loginto a single result slice, which is returned whole and JSON-serialised back to the caller. - Boundary crossed: availability. The controls that exist bound the inputs (
--rpc.rangelimitrejects an over-cap span with a-32000/-32602-class error) but not the output — there is no default cap on the number or byte-size of the returned logs. - Resulting behaviour: response size scales with the matched-log count, which the attacker steers through range breadth and topic selectivity, entirely within whatever range limit is configured. A tiny request yields a megabytes-class response and the scan/serialise work behind it.
Affected systems
- Severity: MEDIUM — availability-only (no funds, no consensus break, no authentication bypass).
- Chain: Ethereum.
- Implementation: go-ethereum (
ethereum/go-ethereum), run with the log-filter RPC exposed and no — or a loose —--rpc.rangelimit, and without a result-size/result-count cap at the RPC edge. - Component / method: JSON-RPC
eth_getLogs(theethnamespace over--http/--ws). - Not affected: a loopback-bound RPC (the go-ethereum default), or a node behind a gateway that enforces both a tight block range and a result-size/count cap. This advisory does not establish a specific affected version range beyond "go-ethereum with the described exposure"; the behaviour is the documented method semantics, not a version-specific regression.
Evidence
- Publicly documented: go-ethereum documents
eth_getLogsas an expensive read and ships--rpc.rangelimitto cap the block span — the mechanism is a property of the documented method, cited toeth/filters/api.go→eth/filters/filter.goin the advisory. - Independently reproduced by NullRabbit (lab): the published corpus reproducer (primitive
eth_getlogs_response_amp, familyresponse_amp,source_class: original) is shipped in the public datasetNullRabbit/nr-bundles-public. It captures the attack traffic — a wide-range/broad-topiceth_getLogsrequest that returns a large log set — contrasted with a narrow, selective benign query to the same method against a local self-owned node. - Measured: not established as a single constant. The amplification is structural: response bytes scale linearly with the number of matching logs, and the attacker controls that count through range breadth and topic selectivity, so the realised factor depends on the target's chain data and its configured range limit rather than a fixed multiplier. The advisory stands on the source trace and go-ethereum's own documentation, not on a measured amplification factor.
Operator actions
- Cap the block range with a tight
--rpc.rangelimit, and additionally cap the result size/count at the RPC edge (a reverse proxy or RPC gateway) — the range limit alone does not bound a single wide-topic response. - Rate-limit
eth_getLogsper source, and require pagination for historical scans. - Do not expose the log-filter RPC unauthenticated on a routable interface. Keep
--http/--wson loopback or behind an authenticating gateway, and expose only the methods an application needs. - Exposure check: confirm whether your
--http/--wsbind address is routable, whether--rpc.rangelimitis set and tight, and whether a result-size cap exists at the edge. Detection ideas (response-size and per-sourceeth_getLogsrate monitoring at the gateway) are suggested controls, not a confirmed vendor fix.
Canonical references
- NRDAX technique: NRDAX-T0329 — Unbounded RPC Response Amplification (class
network-rpc). - NullRabbit advisory: NR-2026-023 — Ethereum (go-ethereum): eth_getLogs wide block-range response amplification → response-amp DoS (published 2026-07-08).
- Evidence bundle (Hugging Face): NullRabbit/nr-bundles-public — primitive
eth_getlogs_response_amp. - Upstream: go-ethereum JSON-RPC documentation (
eth_getLogsas an expensive, range-limited read); source patheth/filters/api.go→eth/filters/filter.go.
Related attacks
eth_getlogs_response_amp is one instance of NRDAX-T0329 (Unbounded RPC Response Amplification), a chain-agnostic technique class the registry records across multiple decentralised-infrastructure RPC surfaces — the shared mechanism being an unpaginated read whose response scales with an attacker-steered match set. Related links here are limited to that shared technique id and the response_amp family; no further relationship is asserted beyond what the registry records.
Track attacks affecting go-ethereum and Ethereum RPC infrastructure.
Analysis plus reproducer. No weaponised proof-of-concept code.
Related Posts
How an exposed debug_traceCall lets one unauthenticated request burn seconds of go-ethereum CPU
debug_traceCall re-runs an eth_call-style message opcode-by-opcode with a full EVM tracer attached, so a single small request converts into up to seconds of single-goroutine CPU on the target — and with no per-source rate or concurrency cap, an unauthenticated caller drives many such traces in parallel against an exposed go-ethereum node.
One prior-knowledge h2c connection multiplexes N eth_getLogs past an L4 per-connection cap on go-ethereum's JSON-RPC port
go-ethereum's JSON-RPC port terminates cleartext HTTP/2 (h2c) by prior knowledge, so one TCP connection can multiplex N eth_getLogs streams past an L4 edge that only caps connections per IP (nginx stream limit_conn) — measured 20/20 streams and ~39 MB pulled through a single connection the edge counts as one, an amplification-multiplexing bypass of a connection cap, not of an L7 request rate limit.
Bypassing go-ethereum's --http.vhosts host allowlist with an empty Host, forged by an HAProxy HTTP/2→1.1 downgrade
An HTTP/2 request with an empty :authority, downgraded to HTTP/1.1 by an HAProxy front, reaches go-ethereum with an empty Host header that its --http.vhosts allowlist accepts (200) though it rejects any concrete disallowed host (403) — a bypass of geth's anti-DNS-rebinding Host filter by an attacker with raw-HTTP/2 reach to the edge.
