Skip to content

fix(subtreevalidation): bound HTTP body reads when fetching subtree data#772

Merged
ordishs merged 3 commits into
bsv-blockchain:mainfrom
ordishs:security/4571-http-body-bound
May 19, 2026
Merged

fix(subtreevalidation): bound HTTP body reads when fetching subtree data#772
ordishs merged 3 commits into
bsv-blockchain:mainfrom
ordishs:security/4571-http-body-bound

Conversation

@ordishs

@ordishs ordishs commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator

Closes #4571.

Summary

util.DoHTTPRequest uses io.ReadAll on the response body with no size limit. Two subtree-fetching callers in services/subtreevalidation/ (getSubtreeTxHashes at SubtreeValidation.go:893 and the missing-subtree path at check_block_subtrees.go:214) accept peer-provided URLs, so a malicious peer can stream up to 60 seconds of unbounded memory allocation per request. Severity HIGH because the attack is remote-triggerable.

Fix

Adds util.DoHTTPRequestBounded(ctx, url, maxBytes, body...) that mirrors DoHTTPRequest but wraps the body in io.LimitReader(body, maxBytes+1) and returns a typed error if the response exceeds maxBytes. Both subtree-fetching call sites now pass maxSubtreeBytes = MaximumMerkleItemsPerSubtree * chainhash.HashSize (32 MB by default).

Other DoHTTPRequest call sites (block fetches, header fetches in services/blockvalidation/) are out of scope — they have different size profiles and may be addressed in separate audit follow-ups.

Test plan

  • Unit tests for DoHTTPRequestBounded: happy path, body equal to limit, body exceeds limit (typed error returned, no over-allocation).
  • Regression test at the call sites — oversized peer response returns typed error.
  • go test -race ./util/... passes.
  • go test -race -tags testtxmetacache ./services/subtreevalidation/... passes.

@github-actions

github-actions Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete

Summary: Security fix correctly addresses HIGH-severity unbounded memory allocation vulnerability. Implementation is sound with comprehensive testing.

Current Review:
No issues found. The fix properly bounds HTTP response body reads when fetching peer-supplied subtree data, preventing remote-triggerable OOM attacks.

Key strengths:

  • Uses io.LimitReader with maxBytes+1 pattern to detect oversized responses without full allocation
  • Calculates limits from policy config (MaximumMerkleItemsPerSubtree * HashSize, default 32 MB)
  • All three vulnerable call sites updated (SubtreeValidation.go:934, check_block_subtrees.go:218, get_blocks.go:664)
  • Returns typed ErrExternal on oversized body for proper error chain handling
  • Comprehensive test coverage: unit tests for boundary conditions (equal, exceed, +1 byte) plus integration tests at each call site
  • Previous inline comment was fixed: test now correctly expects application/octet-stream (23da07e)

Minor observation (not blocking):
Lines http.go:59 and http.go:104 have empty catch blocks for bodyReaderCloser.Close() errors with TODO comments about logging. This matches existing DoHTTPRequest pattern and is out of scope for this security fix.


History:

  • ✅ Fixed: Test assertion mismatch in TestDoHTTPRequestBounded_POST - now correctly expects application/octet-stream (23da07e)

@github-actions

github-actions Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-772 (476a3fd)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 142
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.844µ 1.694µ ~ 0.700
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 62.34n 62.41n ~ 0.500
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 62.23n 62.30n ~ 0.200
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 62.08n 62.08n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 29.30n 29.38n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 49.91n 50.54n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 108.5n 107.5n ~ 0.400
MiningCandidate_Stringify_Short-4 267.6n 270.0n ~ 0.100
MiningCandidate_Stringify_Long-4 1.868µ 1.887µ ~ 0.100
MiningSolution_Stringify-4 947.7n 960.1n ~ 0.100
BlockInfo_MarshalJSON-4 1.759µ 1.774µ ~ 0.100
NewFromBytes-4 129.8n 128.7n ~ 0.700
Mine_EasyDifficulty-4 51.75µ 51.85µ ~ 0.400
Mine_WithAddress-4 5.323µ 5.427µ ~ 0.100
BlockAssembler_AddTx-4 0.02637n 0.02536n ~ 0.400
AddNode-4 10.63 10.91 ~ 1.000
AddNodeWithMap-4 11.44 11.48 ~ 0.700
DiskTxMap_SetIfNotExists-4 3.790µ 3.473µ ~ 0.700
DiskTxMap_SetIfNotExists_Parallel-4 4.024µ 3.324µ ~ 0.100
DiskTxMap_ExistenceOnly-4 461.1n 302.3n ~ 0.100
Queue-4 188.4n 186.5n ~ 0.400
AtomicPointer-4 4.575n 4.701n ~ 0.400
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 909.5µ 855.5µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/10K-4 847.4µ 808.5µ ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/10K-4 122.7µ 103.6µ ~ 0.100
ReorgOptimizations/AllMarkFalse/New/10K-4 62.26µ 61.96µ ~ 0.400
ReorgOptimizations/HashSlicePool/Old/10K-4 65.77µ 53.04µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/10K-4 11.99µ 11.35µ ~ 0.200
ReorgOptimizations/NodeFlags/Old/10K-4 6.100µ 4.639µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 2.690µ 1.578µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 10.252m 9.478m ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/100K-4 10.935m 9.454m ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.186m 1.066m ~ 0.100
ReorgOptimizations/AllMarkFalse/New/100K-4 690.7µ 684.1µ ~ 0.100
ReorgOptimizations/HashSlicePool/Old/100K-4 524.4µ 531.9µ ~ 0.200
ReorgOptimizations/HashSlicePool/New/100K-4 311.3µ 311.5µ ~ 1.000
ReorgOptimizations/NodeFlags/Old/100K-4 51.59µ 47.72µ ~ 0.100
ReorgOptimizations/NodeFlags/New/100K-4 17.39µ 16.35µ ~ 0.100
TxMapSetIfNotExists-4 53.60n 52.05n ~ 0.100
TxMapSetIfNotExistsDuplicate-4 38.75n 38.58n ~ 0.700
ChannelSendReceive-4 634.1n 618.5n ~ 0.100
DirectSubtreeAdd/4_per_subtree-4 55.98n 59.62n ~ 0.700
DirectSubtreeAdd/64_per_subtree-4 29.18n 29.07n ~ 1.000
DirectSubtreeAdd/256_per_subtree-4 27.79n 28.27n ~ 0.200
DirectSubtreeAdd/1024_per_subtree-4 26.50n 26.39n ~ 0.700
DirectSubtreeAdd/2048_per_subtree-4 26.12n 26.06n ~ 0.500
SubtreeProcessorAdd/4_per_subtree-4 295.3n 295.7n ~ 1.000
SubtreeProcessorAdd/64_per_subtree-4 291.2n 293.6n ~ 0.400
SubtreeProcessorAdd/256_per_subtree-4 290.8n 288.2n ~ 0.400
SubtreeProcessorAdd/1024_per_subtree-4 284.0n 278.2n ~ 0.100
SubtreeProcessorAdd/2048_per_subtree-4 281.2n 280.4n ~ 1.000
SubtreeProcessorRotate/4_per_subtree-4 279.2n 281.4n ~ 0.400
SubtreeProcessorRotate/64_per_subtree-4 285.2n 278.8n ~ 0.200
SubtreeProcessorRotate/256_per_subtree-4 284.5n 281.0n ~ 0.100
SubtreeProcessorRotate/1024_per_subtree-4 280.4n 279.8n ~ 1.000
SubtreeNodeAddOnly/4_per_subtree-4 55.66n 54.99n ~ 0.100
SubtreeNodeAddOnly/64_per_subtree-4 36.60n 36.20n ~ 0.200
SubtreeNodeAddOnly/256_per_subtree-4 35.21n 35.10n ~ 0.100
SubtreeNodeAddOnly/1024_per_subtree-4 34.61n 34.49n ~ 0.100
SubtreeCreationOnly/4_per_subtree-4 110.2n 110.1n ~ 1.000
SubtreeCreationOnly/64_per_subtree-4 352.0n 354.8n ~ 0.400
SubtreeCreationOnly/256_per_subtree-4 1.231µ 1.249µ ~ 0.100
SubtreeCreationOnly/1024_per_subtree-4 3.821µ 3.808µ ~ 0.700
SubtreeCreationOnly/2048_per_subtree-4 6.899µ 6.893µ ~ 1.000
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 285.3n 275.1n ~ 0.100
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 288.7n 274.5n ~ 0.100
ParallelGetAndSetIfNotExists/1k_nodes-4 2.002m 2.024m ~ 0.200
ParallelGetAndSetIfNotExists/10k_nodes-4 5.233m 5.266m ~ 0.400
ParallelGetAndSetIfNotExists/50k_nodes-4 7.229m 7.252m ~ 0.700
ParallelGetAndSetIfNotExists/100k_nodes-4 9.757m 10.225m ~ 0.100
SequentialGetAndSetIfNotExists/1k_nodes-4 1.784m 1.796m ~ 0.400
SequentialGetAndSetIfNotExists/10k_nodes-4 4.514m 4.481m ~ 0.700
SequentialGetAndSetIfNotExists/50k_nodes-4 14.12m 14.13m ~ 1.000
SequentialGetAndSetIfNotExists/100k_nodes-4 25.10m 25.35m ~ 1.000
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 2.044m 2.077m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 8.438m 8.523m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 13.25m 13.45m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 1.792m 1.802m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 8.170m 8.076m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 45.05m 43.56m ~ 0.100
CalcBlockWork-4 460.5n 461.9n ~ 0.700
CalculateWork-4 652.0n 626.3n ~ 1.000
BuildBlockLocatorString_Helpers/Size_10-4 1.363µ 1.335µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_100-4 15.68µ 14.74µ ~ 1.000
BuildBlockLocatorString_Helpers/Size_1000-4 128.7µ 127.4µ ~ 0.200
CatchupWithHeaderCache-4 104.4m 104.5m ~ 0.700
_BufferPoolAllocation/16KB-4 3.812µ 3.860µ ~ 0.100
_BufferPoolAllocation/32KB-4 9.898µ 7.738µ ~ 0.700
_BufferPoolAllocation/64KB-4 16.14µ 22.10µ ~ 0.100
_BufferPoolAllocation/128KB-4 30.04µ 32.44µ ~ 1.000
_BufferPoolAllocation/512KB-4 128.1µ 105.0µ ~ 0.100
_BufferPoolConcurrent/32KB-4 19.24µ 19.43µ ~ 1.000
_BufferPoolConcurrent/64KB-4 30.65µ 30.23µ ~ 1.000
_BufferPoolConcurrent/512KB-4 150.4µ 152.1µ ~ 0.400
_SubtreeDeserializationWithBufferSizes/16KB-4 733.3µ 666.8µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 746.8µ 647.1µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 651.9µ 638.7µ ~ 0.700
_SubtreeDeserializationWithBufferSizes/128KB-4 643.0µ 630.4µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 675.6µ 635.8µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 37.68m 36.85m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/32KB-4 38.09m 36.54m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/64KB-4 36.73m 36.56m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/128KB-4 36.53m 36.40m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/512KB-4 36.34m 36.28m ~ 0.100
_PooledVsNonPooled/Pooled-4 738.8n 741.4n ~ 0.200
_PooledVsNonPooled/NonPooled-4 7.543µ 7.479µ ~ 0.100
_MemoryFootprint/Current_512KB_32concurrent-4 6.723µ 6.971µ ~ 0.400
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.672µ 10.177µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.109µ 9.710µ ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.546m 1.370m ~ 0.700
SubtreeSizes/10k_tx_16_per_subtree-4 319.8µ 319.7µ ~ 1.000
SubtreeSizes/10k_tx_64_per_subtree-4 75.84µ 76.68µ ~ 0.700
SubtreeSizes/10k_tx_256_per_subtree-4 18.80µ 18.88µ ~ 1.000
SubtreeSizes/10k_tx_512_per_subtree-4 9.389µ 9.297µ ~ 0.200
SubtreeSizes/10k_tx_1024_per_subtree-4 4.638µ 4.612µ ~ 1.000
SubtreeSizes/10k_tx_2k_per_subtree-4 2.329µ 2.301µ ~ 0.700
BlockSizeScaling/10k_tx_64_per_subtree-4 73.24µ 74.01µ ~ 0.200
BlockSizeScaling/10k_tx_256_per_subtree-4 18.62µ 18.68µ ~ 0.400
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.573µ 4.584µ ~ 0.400
BlockSizeScaling/50k_tx_64_per_subtree-4 386.0µ 391.5µ ~ 0.400
BlockSizeScaling/50k_tx_256_per_subtree-4 93.56µ 92.55µ ~ 0.700
BlockSizeScaling/50k_tx_1024_per_subtree-4 22.93µ 23.14µ ~ 0.200
SubtreeAllocations/small_subtrees_exists_check-4 153.1µ 157.0µ ~ 0.200
SubtreeAllocations/small_subtrees_data_fetch-4 161.6µ 166.9µ ~ 0.100
SubtreeAllocations/small_subtrees_full_validation-4 323.8µ 326.1µ ~ 1.000
SubtreeAllocations/medium_subtrees_exists_check-4 9.011µ 9.240µ ~ 0.100
SubtreeAllocations/medium_subtrees_data_fetch-4 9.516µ 9.640µ ~ 0.100
SubtreeAllocations/medium_subtrees_full_validation-4 18.64µ 18.64µ ~ 0.700
SubtreeAllocations/large_subtrees_exists_check-4 2.174µ 2.190µ ~ 0.700
SubtreeAllocations/large_subtrees_data_fetch-4 2.339µ 2.311µ ~ 0.100
SubtreeAllocations/large_subtrees_full_validation-4 4.679µ 4.817µ ~ 0.400
_prepareTxsPerLevel-4 420.0m 400.0m ~ 0.100
_prepareTxsPerLevelOrdered-4 3.974m 3.649m ~ 0.100
_prepareTxsPerLevel_Comparison/Original-4 422.6m 406.5m ~ 0.100
_prepareTxsPerLevel_Comparison/Optimized-4 4.091m 3.711m ~ 0.100
StoreBlock_Sequential/BelowCSVHeight-4 262.3µ 257.0µ ~ 0.700
StoreBlock_Sequential/AboveCSVHeight-4 259.2µ 258.2µ ~ 0.700
GetUtxoHashes-4 261.4n 260.1n ~ 1.000
GetUtxoHashes_ManyOutputs-4 42.99µ 42.79µ ~ 0.400
_NewMetaDataFromBytes-4 237.8n 236.0n ~ 0.700
_Bytes-4 620.1n 617.3n ~ 0.100
_MetaBytes-4 569.8n 558.9n ~ 0.700

Threshold: >10% with p < 0.05 | Generated: 2026-05-19 10:10 UTC

@ordishs ordishs enabled auto-merge (squash) May 19, 2026 06:49
Comment thread util/http_test.go Outdated

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] Test assertion mismatch

The test expects Content-Type: application/json but DoHTTPRequestBounded sets Content-Type: application/octet-stream for POST requests (util/http.go:288).

This matches existing DoHTTPRequest behavior. Per util/http.go:279-284, binary payloads use application/octet-stream to avoid WAF issues.

Fix: Change line 534 to check for application/octet-stream instead.

ordishs added 3 commits May 19, 2026 11:52
…efs #4571)

Add TestCheckBlockSubtrees_OversizedBody to cover the third peer-fetch
call site at check_block_subtrees.go:218, mirroring the existing
fetchSubtreeFromPeer and getSubtreeTxHashes coverage so all three peer-
fetch paths fail closed on oversized responses.

Align TestDoHTTPRequestBounded_POST content-type assertion with the
application/octet-stream default introduced upstream by bsv-blockchain#829, surfaced
after rebasing onto main.
@ordishs ordishs disabled auto-merge May 19, 2026 09:55
@ordishs ordishs force-pushed the security/4571-http-body-bound branch from 021641a to 23da07e Compare May 19, 2026 09:56
@sonarqubecloud

Copy link
Copy Markdown

@ordishs ordishs merged commit 275a62e into bsv-blockchain:main May 19, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants