fix(net): cap per-peer headers during IBD (header-flood DoS)

Audit #8. The HEADERS handler accepted unbounded headers per peer with no
cumulative cap; during IBD (fCheckPOW=0) a peer could flood cost-free PoW-less
headers into mapBlockIndex/leveldb (never selected -- nMinimumChainWork gates
that -- but still memory/disk growth). Add a per-peer nHeadersProcessed counter
in CNodeState; while IsInitialBlockDownload(), if one peer exceeds
2*max(pindexBestHeader height, checkpoint height) + 200000 headers,
Misbehaving(100) and drop it. The cap is ~2x the chain length, so honest sync
never approaches it; inert post-IBD (the RandomX header check handles forged
headers there).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 18:54:35 -05:00
parent d2124a3038
commit 5951ee118a

View File

@@ -328,6 +328,8 @@ namespace {
bool fBulkHeaderSeen;
//! (server side) time (us) we last served a bulk stream to this peer, for flood throttling.
int64_t nLastBulkServeTime;
//! (#8 IBD header-flood cap) cumulative headers this peer made us process while in IBD.
int64_t nHeadersProcessed;
CNodeState() {
fCurrentlyConnected = false;
@@ -348,6 +350,7 @@ namespace {
nBulkHashStart.SetNull();
fBulkHeaderSeen = false;
nLastBulkServeTime = 0;
nHeadersProcessed = 0;
}
};
@@ -7888,6 +7891,27 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
}
}
// SECURITY (#8: IBD header-flood cap): bound how many headers a single peer can make us
// store while in IBD. Honest headers-first sync needs at most ~chain-length headers from a
// peer; one that floods far past 2x the known chain length is only trying to bloat
// mapBlockIndex/leveldb (such headers are never selected -- nMinimumChainWork gates that --
// but they still cost memory/disk). Cap per-peer and drop the peer. IBD-only: post-IBD the
// RandomX check in AcceptBlockHeader already makes forged headers fail RandomX and ban.
if (IsInitialBlockDownload()) {
CNodeState *hstate = State(pfrom->GetId());
if (hstate != NULL) {
hstate->nHeadersProcessed += (int64_t)nCount;
int knownH = std::max(pindexBestHeader ? (int)pindexBestHeader->GetHeight() : 0,
Checkpoints::GetTotalBlocksEstimate(chainparams.Checkpoints()));
int64_t headerCap = 2 * (int64_t)knownH + 200000;
if (hstate->nHeadersProcessed > headerCap) {
Misbehaving(pfrom->GetId(), 100);
return error("%s: peer=%d flooded %lld headers during IBD (cap %lld)", __func__,
pfrom->id, (long long)hstate->nHeadersProcessed, (long long)headerCap);
}
}
}
if (pindexLast)
UpdateBlockAvailability(pfrom->GetId(), pindexLast->GetBlockHash());