fix: Reject oversized SHAMap nodes in gotStaleData and fetch-pack path

This commit is contained in:
Valentin Balaschenko
2026-07-17 13:07:42 +01:00
committed by Ayaz Salikhov
parent 06a9b1b617
commit 90b2a68da8
3 changed files with 47 additions and 0 deletions

View File

@@ -4,6 +4,7 @@
#include <xrpld/app/main/Application.h>
#include <xrpld/overlay/Peer.h>
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/clock/abstract_clock.h>
#include <xrpl/beast/insight/Collector.h>
@@ -20,6 +21,36 @@
namespace xrpl {
// Per-node cap for AS state leaves stashed via `gotStaleData`.
//
// `gotStaleData` only handles `liAS_NODE` payloads, which carry
// SHAMap state-map leaves (ledger objects).
//
// Sizing: worst-case serialized size across all 31 ledger entry
// types is ~53 KB (`XChainOwnedCreateAccountClaimID`, 256
// attestations x ~209 B, capped by `kMaxAttestations` in
// `include/xrpl/protocol/XChainAttestations.h`), followed by
// `XChainOwnedClaimID` ~40 KB, `NFTokenPage` ~9.5 KB, and
// `LedgerHashes` ~8.2 KB. 256 KiB leaves ~4.8x headroom over the
// current worst case.
//
// Future-proofing: this cap is NOT derived from a single protocol
// constant — it is a soft bound over independently-tuned caps
// (`kMaxAttestations`, `kDirMaxTokensPerPage`, `kMaxTokenUriLength`,
// etc.). Two types (`Amendments`, `NegativeUNL`) have no hard schema
// cap and grow with network state. Revisit if a new object type or
// a lifted array cap approaches ~256 KiB. The downstream
// `SHAMapAccountStateLeafNode` construction rejects anything above
// the 16 MiB SHAMapItem invariant regardless.
inline constexpr std::size_t kMaxFetchPackNodeBytes = 256 * 1024;
// Aggregate cap on the sum of `nodedata().size()` across all entries
// in a single `TMLedgerData` message. Rejects amplification-shaped
// payloads (many nodes, each individually under `kMaxFetchPackNodeBytes`,
// that together dwarf the per-message budget) at ingress in PeerImp,
// before dispatch into `InboundLedger::gotData` or `gotStaleData`.
inline constexpr std::size_t kMaxLedgerDataBytes = megabytes(1);
/**
* Manages the lifetime of inbound ledgers.
*

View File

@@ -259,6 +259,9 @@ public:
if (!node.has_nodeid() || !node.has_nodedata())
return;
if (node.nodedata().size() > kMaxFetchPackNodeBytes)
return;
auto newNode = SHAMapTreeNode::makeFromWire(makeSlice(node.nodedata()));
if (!newNode)

View File

@@ -1699,6 +1699,19 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMLedgerData> const& m)
return;
}
{
std::size_t totalNodeBytes = 0;
for (int i = 0; i < m->nodes_size(); ++i)
totalNodeBytes += m->nodes(i).nodedata().size();
if (totalNodeBytes > kMaxLedgerDataBytes)
{
JLOG(pJournal_.warn())
<< "Ledger data: oversized nodes (" << totalNodeBytes << " bytes)";
fee_.update(Resource::kFeeInvalidData, "oversized ledger nodes");
return;
}
}
// If there is a request cookie, attempt to relay the message
if (m->has_requestcookie())
{