Dev-branch audit (docs/_archive/security-audit-dev-2026-07-22.md) re-found the
master issues (absent on dev) plus new ones in dev-only code. Applied here;
full-node build + test suite green; the subtle fixes were adversarially re-verified.
Ported from the master remediation (adapted to dev's code):
- bootstrap: reject zip-slip / path-traversal archive members (isSafeArchivePath)
before writing (S2-1). (dev already fail-closes on a missing checksum.)
- xmrig updater: fail closed when a signature is required but no key is pinned (F1-1).
- http_download.httpGetString: 16 MiB hard cap + MAXFILESIZE on the shared
metadata/price fetch (F1-2 / caps the updater + exchange paths at one site).
- rpc_client: explicit SSL_VERIFYPEER/VERIFYHOST (F4-1) and a 256 MiB response
cap in WriteCallback (F4-3).
- lite_connection_service: reject remote http:// lite servers, loopback only (L1-1).
Loopback is matched by a strict dotted-decimal 127.0.0.0/8 check (not a
startsWith("127.") prefix, which would wrongly accept 127.0.0.1.evil.com), with
userinfo/fragment stripping.
- lite controller: propagate encrypt/decrypt save() failure instead of reporting
success (F7-1).
- xmrig_manager: chmod(0600) the pool config before writing secrets (F5-2).
- app: clear the copied secret from the OS clipboard on shutdown (F3b-1).
- export_transactions: neutralize CSV/spreadsheet formula injection (F13-1).
- build pipeline: build-from-source lite backend + remove the self-attested
CMake signature gate (F15-1); pinned+verified appimagetool (F15-3/4);
verified Sapling params in setup.sh (F15-6); build.sh exits 0 on success.
(F14-1 empty-quoted-arg and F8-2 NUL-termination were already fixed on dev.)
Dev-only findings:
- rpc_client.callRaw: scrub the raw buffer + parsed tree (templated scrubJsonSecrets
for ordered_json) so console dumpprivkey/z_exportkey keys don't linger in freed
heap (N1-1).
- seed_wallet_creator: wipe the exported mnemonic on the failure path so a discarded
failed result never carries a live seed (W1-2).
- export_all_keys: write the plaintext key dump 0600 + atomically via
writeFileAtomically (U1-2).
- chat_database: restrict chat_messages.sqlite and its WAL/SHM sidecars to owner-only (C3-1).
Not done (need a decision, documented in the report):
- Chat header metadata (cid/z/p) rides outside the AEAD (C1/C2) — binding it is a
wire-protocol change requiring SilentDragonXLite interop review.
- Bootstrap lacks an offline-rooted signature (S2-2 residual) — needs signing infra.
- Console scrollback retains console-typed key-export output in plaintext (N1-1
residual) — inherent to an echoing console; would need output redaction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
119 lines
4.7 KiB
C++
119 lines
4.7 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "http_download.h"
|
|
|
|
#include "logger.h"
|
|
|
|
#include <curl/curl.h>
|
|
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <system_error>
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace dragonx {
|
|
namespace util {
|
|
|
|
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;
|
|
|
|
size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp)
|
|
{
|
|
auto* s = static_cast<std::string*>(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);
|
|
return n;
|
|
}
|
|
|
|
size_t writeFileCb(void* contents, size_t size, size_t nmemb, void* userp)
|
|
{
|
|
return std::fwrite(contents, size, nmemb, static_cast<FILE*>(userp));
|
|
}
|
|
|
|
// Bridge libcurl's C progress callback to the caller's std::function: publish live byte counts
|
|
// and abort the transfer (return non-zero) when the callback returns false (cancellation).
|
|
int xferCb(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t, curl_off_t)
|
|
{
|
|
auto* fn = static_cast<const std::function<bool(double, double)>*>(clientp);
|
|
return (*fn)(static_cast<double>(dlnow), static_cast<double>(dltotal)) ? 0 : 1;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::string httpGetString(const std::string& url, const char* logTag)
|
|
{
|
|
CURL* curl = curl_easy_init();
|
|
if (!curl) return {};
|
|
std::string result;
|
|
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeStringCb);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result);
|
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
|
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
|
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L);
|
|
curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, static_cast<curl_off_t>(kMaxMetadataBytes)); // reject oversized metadata
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
|
const CURLcode res = curl_easy_perform(curl);
|
|
long httpCode = 0;
|
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
|
|
curl_easy_cleanup(curl);
|
|
if (res != CURLE_OK || httpCode < 200 || httpCode >= 300) {
|
|
DEBUG_LOGF("%s GET %s failed: %s (HTTP %ld)\n",
|
|
logTag, url.c_str(), curl_easy_strerror(res), httpCode);
|
|
return {};
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool httpDownloadToFile(const std::string& url, const std::string& destPath,
|
|
const char* logTag, long long maxBytes,
|
|
const std::function<bool(double, double)>& onProgress)
|
|
{
|
|
FILE* fp = std::fopen(destPath.c_str(), "wb");
|
|
if (!fp) return false;
|
|
CURL* curl = curl_easy_init();
|
|
if (!curl) { std::fclose(fp); return false; }
|
|
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFileCb);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
|
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
|
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 0L);
|
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L);
|
|
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1024L);
|
|
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 60L);
|
|
curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, static_cast<curl_off_t>(maxBytes)); // refuse oversized bodies
|
|
// Live progress + cancellation: the callback publishes byte counts and aborts on cancel().
|
|
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
|
|
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xferCb);
|
|
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &onProgress);
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
|
const CURLcode res = curl_easy_perform(curl);
|
|
long httpCode = 0;
|
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
|
|
curl_easy_cleanup(curl);
|
|
std::fclose(fp);
|
|
if (res != CURLE_OK || httpCode < 200 || httpCode >= 300) {
|
|
DEBUG_LOGF("%s download %s failed: %s (HTTP %ld)\n",
|
|
logTag, url.c_str(), curl_easy_strerror(res), httpCode);
|
|
std::error_code ec; fs::remove(destPath, ec);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace util
|
|
} // namespace dragonx
|