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:
@@ -22,16 +22,20 @@ namespace {
|
||||
|
||||
// Cap for in-memory text/JSON responses (release metadata, price/candle data). Far above any
|
||||
// legitimate body, but bounds memory if a hostile/MITM'd server streams an unbounded response.
|
||||
constexpr std::size_t kMaxMetadataBytes = 16u * 1024 * 1024;
|
||||
|
||||
struct StringSink {
|
||||
std::string* body;
|
||||
long long cap;
|
||||
};
|
||||
|
||||
size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp)
|
||||
{
|
||||
auto* s = static_cast<std::string*>(userp);
|
||||
auto* sink = static_cast<StringSink*>(userp);
|
||||
const size_t n = size * nmemb;
|
||||
// Hard cap: a chunked response omits Content-Length, so CURLOPT_MAXFILESIZE cannot catch it —
|
||||
// aborting here (short count) makes curl fail the transfer instead of exhausting memory.
|
||||
if (s->size() + n > kMaxMetadataBytes) return 0;
|
||||
s->append(static_cast<char*>(contents), n);
|
||||
if (httpSinkAccept(sink->body->size(), n, sink->cap) != n) return 0;
|
||||
sink->body->append(static_cast<char*>(contents), n);
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -68,13 +72,15 @@ HttpGetResult httpGet(const std::string& url, const char* logTag, const HttpGetO
|
||||
if (!curl) { out.error = "failed to initialize curl"; return out; }
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeStringCb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &out.body);
|
||||
StringSink sink{&out.body, opt.maxBytes};
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &sink);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, opt.maxRedirects);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, opt.timeoutSec);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, opt.connectTimeoutSec);
|
||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); // no SIGALRM for timeouts: these run on worker threads
|
||||
curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, static_cast<curl_off_t>(kMaxMetadataBytes)); // reject oversized metadata
|
||||
curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, static_cast<curl_off_t>(opt.maxBytes)); // reject oversized metadata
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
if (opt.cancel) {
|
||||
|
||||
Reference in New Issue
Block a user