fix(net): cap market and bootstrap HTTP response bodies

Several HTTP fetches read a remote body into memory without a tight
bound. The price and exchange-ticker calls shared one large 16 MiB
metadata cap, and the bootstrap checksum download had no size limit at
all, so a hostile or MITM'd server streaming an unbounded (and
Content-Length-free) response could grow memory far past anything these
endpoints legitimately return.

- The HTTP GET helper now takes a per-call byte cap and redirect limit.
  The write callback refuses bytes past the cap, which aborts the
  transfer, and the same value is set as curl's max file size.
- The price call is capped at 256 KiB and the ticker list at 2 MiB,
  each sized to its real payload rather than the shared default.
- The bootstrap checksum fetch caps its body at 64 KiB in both the
  write callback and curl's max file size.

Tests cover the sink accept/abort boundary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 19:24:12 -05:00
co-authored by Claude Opus 4.8
parent edbb98d9ec
commit f1b0ae5ce7
5 changed files with 44 additions and 8 deletions
+6 -2
View File
@@ -3201,7 +3201,9 @@ void App::refreshPrice()
NetworkRefreshService::PriceHttpResult result;
try {
const char* const url = "https://api.coingecko.com/api/v3/simple/price?ids=dragonx-2&vs_currencies=usd,btc&include_24hr_change=true&include_24hr_vol=true&include_market_cap=true";
util::HttpGetResult r = util::httpGet(url, "[price]", marketHttp(&http_cancel_, 10));
util::HttpGetOptions priceOpt = marketHttp(&http_cancel_, 10);
priceOpt.maxBytes = 256 * 1024;
util::HttpGetResult r = util::httpGet(url, "[price]", priceOpt);
NetworkRefreshService::PriceHttpResponse response{
r.transportOk, r.httpStatus, std::move(r.body), r.error};
result = NetworkRefreshService::parsePriceHttpResponse(response, std::time(nullptr));
@@ -3433,9 +3435,11 @@ void App::refreshExchanges()
http_worker_->post([this]() -> rpc::RPCWorker::MainCb {
// httpGetString verifies TLS, caps the response size and returns "" on any failure.
util::HttpGetOptions tickersOpt = marketHttp(&http_cancel_, 12);
tickersOpt.maxBytes = 2 * 1024 * 1024;
const std::string body = util::httpGetString(
"https://api.coingecko.com/api/v3/coins/dragonx-2/tickers", "[tickers]",
marketHttp(&http_cancel_, 12));
tickersOpt);
std::vector<data::ExchangeInfo> exchanges;
if (!body.empty()) {
try { exchanges = data::parseCoinGeckoTickers(body); } catch (...) {}