Zebra getaddressutxos RPC response amplification
Zebra's getaddressutxos RPC method returns all unspent outputs for a transparent address without pagination, enabling an unauthenticated attacker to amplify a small request into a large response (≈2,033×) by pre-seeding the address with dust UTXOs.
Why this matters
Operators exposing Zebra's RPC interface face a bandwidth and memory exhaustion risk. Unlike getaddressbalance (which returns a scalar), getaddressutxos scales linearly with the number of UTXOs. A naïve request-count rate limit is insufficient because a single request can consume significant resources, bounded per query only by rpc.max_response_body_size (default 50 MiB). While the sibling method getaddresstxids exhibits similar unpaginated behaviour, getaddressutxos presents a higher per-item byte size (~235 B/UTXO vs ~67 B/txid), resulting in greater amplification potential for the same number of seeded items.
How the attack works
The attacker controls a transparent address and seeds it with N cheap coinbase UTXOs. They then invoke the getaddressutxos method via zebra-rpc. The handler (methods.rs → zebra_state::ReadRequest::UtxosByAddresses) retrieves all UTXOs for that address and constructs a JSON response containing one entry per UTXO (fields: address, txid, outputIndex, script, satoshis, height). The response size scales linearly with N. The request is unauthenticated and small (~116 B), while the response is bounded only by rpc.max_response_body_size.
Affected systems
- Severity: Medium (per NR-2026-061).
- Implementation: Zebra (
ZcashFoundation/zebra) - Component:
zebra-rpc - Method:
getaddressutxos - Version: Observed on Zebra 4.2.0 (Regtest). The behaviour is documented unpaginated method semantics, not a version-specific regression.
Evidence
- Publicly documented: The Zcash RPC documentation lists
getaddressutxosas returning a list of UTXOs without pagination parameters. - Independently reproduced by NullRabbit: A reproducer (
zcash_zebra_rpc_amp.py) was run against a self-owned Zebra 4.2.0 Regtest node. - Measured under controlled conditions:
- Environment: Zebra 4.2.0 Regtest, loopback, N=1000 coinbase UTXOs at a controlled transparent address.
- Result: ~116 B request → ~236 KB response.
- Amplification: ≈2,033×.
- Linearity: Confirmed at N=200 (406×) and N=1000 (2,033×).
- Control:
getaddressbalancereturned a scalar (≈1× amplification).
Operator actions
- Mitigation: Paginate or cap the result count for address-index RPCs. Lower
rpc.max_response_body_size. Implement rate limiting per source IP. Ensure the RPC is bound to loopback or placed behind an authenticating gateway. - Detection: Monitor for spikes in RPC response sizes or high bandwidth usage associated with
getaddressutxoscalls to specific transparent addresses.
Canonical references
- NRDAX technique: NRDAX-T0329 (family
response_amp). - NullRabbit advisory: NR-2026-061 — Zebra transparent-address index RPC response amplification (published 2026-07-15).
- Evidence bundle (Hugging Face): NullRabbit/nr-bundles-public — primitive
zcash_zebra_getaddressutxos_response_amp. - Reproducer:
chains/zcash/lab/drivers/zcash_zebra_rpc_amp.py. - Upstream primary source: Zcash getaddressutxos RPC docs (documented unpaginated list-RPC semantics).
Related attacks
- Sibling finding (same advisory):
zcash_zebra_getaddresstxids_response_amp(method:getaddresstxids). This method also lacks pagination but returns smaller per-item payloads (~67 B/txid vs ~235 B/UTXO), resulting in lower measured amplification (~578× at N=1000). - Same technique (NRDAX-T0329): Other Zebra instances such as
zcash_zebra_getblocktemplate_response_ampandzcash_zebra_getrawmempool_verbose_response_ampexist within the same technique family.
Subscription call to action
Track attacks affecting Zebra.
Analysis plus reproducer. No weaponised proof-of-concept code.
Related Posts
Zebra getaddresstxids RPC response amplification
Zebra's getaddresstxids RPC returns every transaction identifier for a queried transparent address without pagination, allowing a ~116-byte request to trigger a response that scales with the number of transactions at the address, measured at ~578× amplification in controlled testing.
Amplifying go-ethereum RPC responses with an eth_simulateV1 block-number gap
A single unauthenticated eth_simulateV1 call with a gap between two block numbers makes go-ethereum synthesise up to ~256 full block headers, expanding a ~285-byte request into a ~434 KB response.
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 filter across the requested block span in one JSON-RPC response, so a tiny wide-range or broad-topic request forces an exposed go-ethereum node to scan the range and serialise an unbounded matched-log set — a response-amplification DoS payable by an unauthenticated caller.
