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>
177 lines
5.9 KiB
C++
177 lines
5.9 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "export_transactions_dialog.h"
|
|
#include "../../app.h"
|
|
#include "../../util/i18n.h"
|
|
#include "../../util/platform.h"
|
|
#include "../notifications.h"
|
|
#include "../schema/ui_schema.h"
|
|
#include "../material/draw_helpers.h"
|
|
#include "../material/type.h"
|
|
#include "../widgets/copy_field.h"
|
|
#include "../theme.h"
|
|
#include "../../embedded/IconsMaterialDesign.h"
|
|
#include "imgui.h"
|
|
|
|
#include <string>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <ctime>
|
|
#include <iomanip>
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
|
|
// Static state
|
|
static bool s_open = false;
|
|
static char s_filename[256] = "";
|
|
static std::string s_saved_path; // full path on success (shown as a copy field); empty otherwise
|
|
static std::string s_error; // error message on failure; empty otherwise
|
|
// Re-entrancy guard: true while a CSV write is in flight (disables the
|
|
// Export button so a second synchronous write can't be kicked off).
|
|
static bool s_exporting = false;
|
|
|
|
// Helper to escape CSV field
|
|
static std::string escapeCSV(const std::string& field)
|
|
{
|
|
// Neutralize spreadsheet formula injection: a field beginning with '=', '+', '-', '@',
|
|
// tab, or CR is interpreted as a formula by Excel/LibreOffice, letting an attacker-supplied
|
|
// memo/address execute on open. Prefix such fields with a single quote so they render as text.
|
|
std::string safe = field;
|
|
if (!safe.empty()) {
|
|
const char c0 = safe.front();
|
|
if (c0 == '=' || c0 == '+' || c0 == '-' || c0 == '@' || c0 == '\t' || c0 == '\r')
|
|
safe.insert(safe.begin(), '\'');
|
|
}
|
|
if (safe.find(',') != std::string::npos ||
|
|
safe.find('"') != std::string::npos ||
|
|
safe.find('\n') != std::string::npos) {
|
|
// Escape quotes and wrap in quotes
|
|
std::string escaped;
|
|
escaped.reserve(safe.size() + 4);
|
|
escaped += '"';
|
|
for (char c : safe) {
|
|
if (c == '"') escaped += "\"\"";
|
|
else escaped += c;
|
|
}
|
|
escaped += '"';
|
|
return escaped;
|
|
}
|
|
return safe;
|
|
}
|
|
|
|
void ExportTransactionsDialog::show()
|
|
{
|
|
s_open = true;
|
|
s_saved_path.clear();
|
|
s_error.clear();
|
|
|
|
// Generate default filename with timestamp
|
|
std::time_t now = std::time(nullptr);
|
|
char timebuf[32];
|
|
std::strftime(timebuf, sizeof(timebuf), "%Y%m%d_%H%M%S", std::localtime(&now));
|
|
snprintf(s_filename, sizeof(s_filename), "dragonx_transactions_%s.csv", timebuf);
|
|
}
|
|
|
|
bool ExportTransactionsDialog::isOpen()
|
|
{
|
|
return s_open;
|
|
}
|
|
|
|
void ExportTransactionsDialog::hide()
|
|
{
|
|
s_open = false;
|
|
s_saved_path.clear();
|
|
s_error.clear();
|
|
}
|
|
|
|
void ExportTransactionsDialog::render(App* app)
|
|
{
|
|
if (!s_open) return;
|
|
namespace m = material;
|
|
|
|
m::OverlayDialogSpec ov;
|
|
ov.title = TR("export_tx_title");
|
|
ov.p_open = &s_open;
|
|
ov.style = m::OverlayStyle::BlurFloat;
|
|
ov.cardWidth = 520.0f;
|
|
ov.idSuffix = "exporttx";
|
|
if (!m::BeginOverlayDialog(ov)) return;
|
|
|
|
const auto& state = app->getWalletState();
|
|
|
|
ImGui::Text(TR("export_tx_count"), state.transactions.size());
|
|
ImGui::Spacing();
|
|
|
|
// Filename + save-location hint.
|
|
m::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
|
|
ImGui::Spacing();
|
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceDisabled()), "%s", TR("file_save_location"));
|
|
|
|
// Result — success shows the saved path as a copy field; failure shows a red line.
|
|
if (!s_saved_path.empty()) {
|
|
ImGui::Spacing();
|
|
ImGui::PushFont(m::Type().iconSmall());
|
|
ImGui::TextColored(m::SuccessVec4(), ICON_MD_CHECK_CIRCLE);
|
|
ImGui::PopFont();
|
|
ImGui::SameLine(0, 4.0f * Layout::dpiScale());
|
|
ImGui::TextColored(m::SuccessVec4(), "%s", TR("export_tx_success"));
|
|
widgets::AddressCopyField("##exportedpath", s_saved_path);
|
|
} else if (!s_error.empty()) {
|
|
ImGui::Spacing();
|
|
ImGui::PushTextWrapPos(0.0f);
|
|
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", s_error.c_str());
|
|
ImGui::PopTextWrapPos();
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
|
|
bool doExport = false, doClose = false;
|
|
m::DialogActionFooter(TR("export"), !s_exporting, TR("close"), doExport, doClose);
|
|
if (doClose) s_open = false;
|
|
if (doExport) {
|
|
s_saved_path.clear();
|
|
s_error.clear();
|
|
if (state.transactions.empty()) {
|
|
Notifications::instance().warning(TR("export_tx_none"));
|
|
} else {
|
|
s_exporting = true;
|
|
std::string configDir = util::Platform::getConfigDir();
|
|
std::string filepath = configDir + "/" + s_filename;
|
|
|
|
std::ofstream file(filepath);
|
|
if (!file.is_open()) {
|
|
s_error = TR("export_tx_file_fail");
|
|
Notifications::instance().error(TR("export_tx_file_fail"));
|
|
} else {
|
|
file << "Date,Type,Amount,Address,TXID,Confirmations,Memo\n";
|
|
for (const auto& tx : state.transactions) {
|
|
std::time_t t = static_cast<std::time_t>(tx.timestamp);
|
|
char datebuf[32];
|
|
std::strftime(datebuf, sizeof(datebuf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
|
|
file << datebuf << ",";
|
|
file << escapeCSV(tx.type) << ",";
|
|
std::ostringstream amt;
|
|
amt << std::fixed << std::setprecision(8) << tx.amount;
|
|
file << amt.str() << ",";
|
|
file << escapeCSV(tx.address) << ",";
|
|
file << escapeCSV(tx.txid) << ",";
|
|
file << tx.confirmations << ",";
|
|
file << escapeCSV(tx.memo) << "\n";
|
|
}
|
|
file.close();
|
|
s_saved_path = filepath;
|
|
Notifications::instance().success(TR("export_tx_success"), 5.0f);
|
|
}
|
|
s_exporting = false;
|
|
}
|
|
}
|
|
|
|
m::EndOverlayDialog();
|
|
}
|
|
|
|
} // namespace ui
|
|
} // namespace dragonx
|