Cardano `cardano-submit-api`: unbounded request body → memory exhaustion
cardano-submit-api, the HTTP transaction submission service shipped with Cardano node, starts its Warp HTTP server without a request-body-size limit, causing unauthenticated POST /api/submit/tx requests with oversized bodies to be buffered entirely in memory before rejection, pinning ~500 MB resident set size per request and potentially causing out-of-memory termination on commodity validator infrastructure under concurrent attack.
Why this matters
Cardano operators typically deploy cardano-submit-api alongside relay and staking nodes to provide public HTTP transaction submission endpoints. The missing body-size cap allows any remote attacker to send arbitrarily large POST requests that the service must buffer before attempting to decode or reject. A modest number of concurrent oversized submissions (~32 × 500 MB ≈ 16 GB) can OOM-kill the service on standard 16 GB validator infrastructure, blocking transaction submissions through that operator's endpoint while leaving consensus unaffected. A simple rate limit based on HTTP requests per second does not address this memory-amplification attack, as memory consumption scales with body size rather than request count.
How the attack works
The vulnerability originates in cardano-submit-api/src/Cardano/TxSubmit/Rest/Types.hs:21-24:
toWarpSettings :: WebserverConfig -> Warp.Settings
toWarpSettings WebserverConfig {wcHost, wcPort} =
Warp.defaultSettings & Warp.setHost wcHost & Warp.setPort wcPort
toWarpSettings chains only setHost and setPort onto Warp.defaultSettings. There is no setMaximumBodyFlush or equivalent body-size limit configured. Warp's defaults do not impose a request-body size limit; applications must enforce one, and here it does not.
The submission handler, defined in cardano-submit-api/src/Cardano/TxSubmit/Types.hs:118-124, uses Servant's ReqBody '[CBORStream] ByteString pattern, which buffers the entire request body into a strict ByteString before the handler executes:
newtype TxSubmitApiRecord route = TxSubmitApiRecord
{ _txSubmitPost :: route
:- "submit"
:> "tx"
:> ReqBody '[CBORStream] ByteString
:> PostAccepted '[JSON] TxId
}
When an attacker sends POST /api/submit/tx with an oversized Content-Length and body, Warp reads and buffers the full body before Servant invokes the handler, regardless of whether the eventual transaction is valid CBOR or not. Memory scales linearly with attacker-supplied body size. The measured ~500 MB resident set size pin per request represents the committed memory allocation, and RSS does not fully recover between attacks in testing, suggesting Haskell's garbage collector does not aggressively release allocation chunks before subsequent buffers arrive.
Affected systems
Affected component: cardano-node shipping cardano-submit-api with default Warp settings. Source trace verified against https://github.com/IntersectMBO/cardano-node/blob/master/cardano-submit-api/src/Cardano/TxSubmit/Rest/Types.hs. No specific version ranges are excluded in the source; the defect exists where toWarpSettings does not set a body-size limit. Testing used apexpool/cardano-submit-api:latest Docker image (community port matching upstream cabal pin) and binary HEAD verification; the fix shape applies to all versions using the same toWarpSettings pattern.
Evidence
Publicly documented: none — no independent CVE or GHSA covers this finding. The mechanism, source trace, and measurement are documented in NullRabbit's own advisory NR-2026-013 (see Canonical references), not by any third party.
Independently reproduced by NullRabbit: a loopback reproducer in chains/cardano/lab/drivers/known_class_cardano_body.py sends representative oversized bodies against a no-cap HTTP listener that reads the full body before rejection, capturing the wire signature of the attack. Three shipped bundles on nr-bundles-public (corpus IDs crp_6189539811f346e1, crp_b9d6a79fafb14643, crp_5c51c21cc77d456c) each stamp wire_fidelity=scaled_body_representative and record the measured RSS in attack_parameters.
Measured under controlled conditions: container baseline RSS 22 MB. Tested body sizes 1 MB to 500 MB with HTTP status 400 throughout (cardano-node socket unreachable in test environment; the 400 status occurs after body buffering completes). Measured RSS growth: 1 MB → +54 MB, 10 MB → +20 MB, 50 MB → +94 MB, 100 MB → +133 MB, 250 MB → +383 MB, 500 MB → +444 MB. Cumulative RSS growth across six attack bursts: 22 MB → 1,150 MB = +1.13 GB committed. RSS does not fully recover between bursts. In production where the socket is reachable, the body is still buffered before CBOR decode; the buffer commit is independent of downstream handler outcome.
Inferred but not reproduced: the "~32 × 500 MB ≈ 16 GB → OOM on 16 GB infrastructure" figure is an extrapolation from the measured single-request RSS pin, not a measured multi-attacker OOM. Concurrent-attacker OOM was not reproduced.
Operator actions
Enforce a request-body-size limit at the Warp layer by modifying toWarpSettings to call setMaximumBodyFlush with an appropriate threshold, or reject early on Content-Length exceeding the threshold before buffering. A submitted transaction has a well-bounded maximum size, so the cap can be tight. Alternatively, front the service with a reverse proxy (nginx client_max_body_size or equivalent) that rejects oversized bodies before they reach cardano-submit-api. Where deployment permits, bind cardano-submit-api to a trusted interface rather than publicly. These mitigations address the memory amplification at the network entry point rather than attempting resource limits inside the Haskell runtime.
Canonical references
- NRDAX technique: NRDAX-T0328 — unbounded-request-body-memory-exhaustion (family
memory_amp; active, first seen 2026-07-07). - NullRabbit advisory: NR-2026-013 — Cardano
cardano-submit-api: no request-body-size cap → memory exhaustion (published 2026-07-06). - Evidence bundle (Hugging Face): NullRabbit/nr-bundles-public — primitive
cardano_submit_api_body_memory_pin(3 bundles shipped). - Reproducer:
chains/cardano/lab/drivers/known_class_cardano_body.py— representative 16 MB body capture, measured ~500 MB RSS per request recorded inattack_parameters. - Upstream primary source:
IntersectMBO/cardano-nodecardano-submit-api/src/Cardano/TxSubmit/Rest/Types.hs(toWarpSettings— the missing body-size limit).
Related attacks
NRDAX-T0328 spans multiple chains under the same unbounded-request-body-memory-exhaustion technique. The Cardano instance (cardano_submit_api_body_memory_pin) shares the technique with ic_orchestrator_cup_body_bomb (Internet Computer), prysm_attester_slashing_pubkey_burn (Ethereum Prysm), prysm_bls_exec_change_decode_burn (Ethereum Prysm), prysm_bls_pool_decode_burn (Ethereum Prysm), reth_pooledtx_decode_memory_amp (Ethereum reth), and monero_wallet_rpc_body_size_dos (Monero). The brief describes only the Cardano instance.
Subscription call to action
Track attacks affecting Cardano cardano-submit-api.
Analysis plus reproducer. No weaponised proof-of-concept code.
Related Posts
How a zero-value GetBlockHeaders request underflows to serve go-ethereum's entire header chain (CVE-2024-32972)
A go-ethereum eth-protocol peer that sends a GetBlockHeaders request with amount (count) set to 0 triggers an integer underflow — count-1 wraps to UINT64_MAX — bypassing the maxHeadersServe cap and forcing the node to serve headers back to genesis, a publicly disclosed memory-exhaustion DoS (CVE-2024-32972, fixed in geth 1.13.15) that NullRabbit reproduced as a lab detection bundle.
How unbounded HTTP/2 concurrent streams let one TCP connection OOM-kill an IOTA node's public gRPC server
An IOTA node's public gRPC server accepts an unbounded number of concurrent HTTP/2 streams on a single TCP connection, so one client multiplexing around 200 concurrent GetTransactions calls drives resident memory from a few hundred MB to multiple GB in about 10 seconds until the kernel OOM-killer terminates the node.
Beyond Zero answers what the machine decides. It does not answer who said it could.
Google's Beyond Zero moves the trust boundary from the application to the individual action and puts a reasoning layer in the access path. The objection that landed on Hacker News, that a non-deterministic evaluator does not belong there, is aimed one layer too low. The gap is not determinism. It is legitimacy: nothing establishes that a given system has the standing to act in your environment, against your traffic, under your conditions. Vendor-asserted accuracy is not evidence.
