← Back to Research
Research · July 28, 2026

Cardano `cardano-submit-api`: unbounded request body → memory exhaustion

Simon Morley·5 min read

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

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.

cardanocardano-submit-apihttpmemory_ampinfrastructure-security
Publication policy:Why we publish these findings →

Related Posts

How a zero-value GetBlockHeaders request underflows to serve go-ethereum's entire header chain (CVE-2024-32972)

·7 min read

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.

ethereumgo-ethereumdevp2p+4 more

Beyond Zero answers what the machine decides. It does not answer who said it could.

·5 min read

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.

earned-autonomyzero-trustbeyond-zero+6 more