fix(parsers): harden RPC/price/updater parsing against valid-but-unhandled input

Audit of the full-node RPC response parsers and updater release-body parsers
(find -> adversarially-verify workflow) surfaced three worth fixing; three
others guard formats the project doesn't emit and are backstopped by signature
verification, so they're documented rather than churned.

- Price (Medium): parseCoinGeckoPriceResponse used .value(key, 0.0), which
  throws type_error on a PRESENT null. CoinGecko emits null for usd_24h_change/
  usd_24h_vol on illiquid tokens (DRGX is one) while still returning a valid
  spot price; the outer catch turned that into no price update at all. Read
  null-tolerantly so the valid usd/btc survives.

- Daemon updater (Medium): parseDaemonChecksums blanked '|'/backtick but not
  markdown emphasis, so a bolded **archive.zip** checksum row was dropped and a
  valid, correctly-signed release would be refused. Also blank '*'/'_' (cannot
  cause a wrong-asset match; the 64-hex + .zip-suffix tests are unchanged).

- Opid poll (Low, severe failure mode): parseOperationStatusPoll read id/status
  via .value() (throws on a present non-string) and the call site parsed OUTSIDE
  its try/catch, so a throw left opid_poll_in_progress_ stuck true and wedged all
  z-operation polling for the session. Type-check the reads and parse inside the
  guard. (dragonxd can't emit non-string id/status; this is defense-in-depth.)

Regression tests: CoinGecko null field; opid non-string id/status; **bold**
checksum row. Suite green (1/1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-09 13:38:50 -05:00
parent a1d3964e34
commit 5296dd7ae5
4 changed files with 63 additions and 12 deletions

View File

@@ -1129,6 +1129,7 @@ void App::update()
auto* rpc = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
if (!rpc) return [this](){ opid_poll_in_progress_ = false; };
json result;
services::NetworkRefreshService::OperationStatusPollResult parsed;
try {
rpc::RPCClient::TraceScope trace("Send tab / Operation status");
// No per-opid filter: this daemon rejects z_getoperationstatus(["opid"]) with
@@ -1136,10 +1137,12 @@ void App::update()
// "Waiting for operation". The no-arg form returns ALL operations;
// parseOperationStatusPoll() filters down to the opids we're tracking.
result = rpc->call("z_getoperationstatus", json::array());
// Parse INSIDE the guard: a malformed/type-anomalous element must never abort the
// poll and leave opid_poll_in_progress_ stuck true for the whole connected session.
parsed = services::NetworkRefreshService::parseOperationStatusPoll(result, opids);
} catch (...) {
return [this](){ opid_poll_in_progress_ = false; };
}
auto parsed = services::NetworkRefreshService::parseOperationStatusPoll(result, opids);
return [this, parsed = std::move(parsed)]() mutable {
opid_poll_in_progress_ = false;