Six bounded defensive fixes: - Payment-URI amount is now strictly validated (reject NaN/inf/sign/garbage, <=8 decimals, 0..21,000,000) at parse time, so a crafted QR/URI can't inject a bogus amount into the Send form. The z_sendmany call the user confirms is unchanged; this is input validation only. - export-all-keys filename is sanitized (basename-only, no traversal) via a shared util::sanitizeExportFilename (factored out of the CSV export dialog). - Chat append()/upsert() wrap the strict serialize in try/catch so a malformed (invalid-UTF-8, attacker-influenceable) body is skipped, not crashed on — matching appendBatch's existing drop behavior. - http_download (used by the updaters + pool stats) restricts transfers AND redirects to HTTPS and bounds the download path's redirects, so a redirect can't downgrade to http:// or another scheme. - The default DRGX.conf (rpcuser/rpcpassword) is written 0600 from the start via writeFileAtomically, closing the brief world-readable ofstream+chmod window. - Auto-lock is suppressed only for SOLO mining (needs the wallet), not pool mining (pays an external address), which was holding the wallet open forever. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
213 lines
6.7 KiB
C++
213 lines
6.7 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "payment_uri.h"
|
|
#include "address_validation.h"
|
|
|
|
#include <sstream>
|
|
#include <iomanip>
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cstdio>
|
|
#include <cmath>
|
|
|
|
namespace {
|
|
// Strictly parse a decimal DRGX amount from an (attacker-influenceable) payment URI / QR: digits with at
|
|
// most one '.', at most 8 fractional digits, in [0, 21000000]. Rejects empty, NaN/inf, signs, whitespace
|
|
// and trailing garbage that std::stod would otherwise accept — so a crafted URI can't inject a bogus
|
|
// amount into the Send form.
|
|
bool parseStrictAmount(const std::string& s, double& out) {
|
|
if (s.empty() || s.size() > 32) return false;
|
|
int dots = 0, digits = 0, frac = 0; bool afterDot = false;
|
|
for (char c : s) {
|
|
if (c == '.') { if (++dots > 1) return false; afterDot = true; }
|
|
else if (c >= '0' && c <= '9') { ++digits; if (afterDot && ++frac > 8) return false; }
|
|
else return false;
|
|
}
|
|
if (digits == 0) return false;
|
|
double v = 0.0;
|
|
try { v = std::stod(s); } catch (...) { return false; }
|
|
if (!std::isfinite(v) || v < 0.0 || v > 21000000.0) return false;
|
|
out = v;
|
|
return true;
|
|
}
|
|
} // namespace
|
|
|
|
namespace dragonx {
|
|
namespace util {
|
|
|
|
std::string urlEncode(const std::string& value)
|
|
{
|
|
std::string out;
|
|
out.reserve(value.size());
|
|
for (unsigned char c : value) {
|
|
if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
|
|
out += static_cast<char>(c);
|
|
} else if (c == ' ') {
|
|
out += "%20";
|
|
} else {
|
|
char hex[4];
|
|
std::snprintf(hex, sizeof(hex), "%%%02X", c);
|
|
out += hex;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::string buildPaymentUri(const std::string& address, double amount,
|
|
const std::string& label, const std::string& memo)
|
|
{
|
|
if (address.empty()) return "";
|
|
|
|
std::ostringstream uri;
|
|
uri << "drgx:" << address;
|
|
|
|
bool hasParams = false;
|
|
auto addParam = [&](const char* key, const std::string& value) {
|
|
if (value.empty()) return;
|
|
uri << (hasParams ? "&" : "?") << key << "=" << value;
|
|
hasParams = true;
|
|
};
|
|
|
|
if (amount > 0.0) {
|
|
std::ostringstream amt;
|
|
amt << std::fixed << std::setprecision(8) << amount;
|
|
addParam("amount", amt.str());
|
|
}
|
|
addParam("label", urlEncode(label));
|
|
addParam("memo", urlEncode(memo));
|
|
|
|
return uri.str();
|
|
}
|
|
|
|
std::string urlDecode(const std::string& encoded)
|
|
{
|
|
std::string result;
|
|
result.reserve(encoded.size());
|
|
|
|
for (size_t i = 0; i < encoded.size(); i++) {
|
|
if (encoded[i] == '%' && i + 2 < encoded.size()) {
|
|
// Decode hex pair
|
|
char hex[3] = { encoded[i + 1], encoded[i + 2], '\0' };
|
|
char* end;
|
|
long val = strtol(hex, &end, 16);
|
|
if (end == hex + 2) {
|
|
result += static_cast<char>(val);
|
|
i += 2;
|
|
continue;
|
|
}
|
|
} else if (encoded[i] == '+') {
|
|
result += ' ';
|
|
continue;
|
|
}
|
|
result += encoded[i];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool isPaymentURI(const std::string& str)
|
|
{
|
|
// Check for drgx: or hush: prefix (case insensitive)
|
|
std::string lower = str;
|
|
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
|
|
return lower.substr(0, 5) == "drgx:" || lower.substr(0, 5) == "hush:";
|
|
}
|
|
|
|
PaymentURI parsePaymentURI(const std::string& uri)
|
|
{
|
|
PaymentURI result;
|
|
result.valid = false;
|
|
|
|
// Check prefix
|
|
if (!isPaymentURI(uri)) {
|
|
result.error = "Invalid URI scheme. Expected drgx: or hush:";
|
|
return result;
|
|
}
|
|
|
|
// Skip the scheme prefix (drgx: or hush:)
|
|
size_t schemeEnd = uri.find(':');
|
|
if (schemeEnd == std::string::npos) {
|
|
result.error = "Invalid URI format";
|
|
return result;
|
|
}
|
|
|
|
std::string remainder = uri.substr(schemeEnd + 1);
|
|
|
|
// Handle double-slash format (drgx://address)
|
|
if (remainder.size() >= 2 && remainder[0] == '/' && remainder[1] == '/') {
|
|
remainder = remainder.substr(2);
|
|
}
|
|
|
|
// Split address and query string
|
|
size_t queryStart = remainder.find('?');
|
|
if (queryStart == std::string::npos) {
|
|
// No query parameters, just the address
|
|
result.address = remainder;
|
|
} else {
|
|
result.address = remainder.substr(0, queryStart);
|
|
std::string queryString = remainder.substr(queryStart + 1);
|
|
|
|
// Parse query parameters
|
|
std::istringstream queryStream(queryString);
|
|
std::string param;
|
|
|
|
while (std::getline(queryStream, param, '&')) {
|
|
size_t eqPos = param.find('=');
|
|
if (eqPos == std::string::npos) continue;
|
|
|
|
std::string key = param.substr(0, eqPos);
|
|
std::string value = urlDecode(param.substr(eqPos + 1));
|
|
|
|
// Convert key to lowercase for case-insensitive matching
|
|
std::transform(key.begin(), key.end(), key.begin(), ::tolower);
|
|
|
|
if (key == "amount" || key == "amt") {
|
|
double a = 0.0;
|
|
if (!parseStrictAmount(value, a)) {
|
|
result.error = "Invalid amount";
|
|
result.valid = false;
|
|
return result; // reject the whole URI rather than silently treating it as 0
|
|
}
|
|
result.amount = a;
|
|
} else if (key == "label") {
|
|
result.label = value;
|
|
} else if (key == "memo") {
|
|
result.memo = value;
|
|
} else if (key == "message" || key == "msg") {
|
|
result.message = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate address
|
|
if (result.address.empty()) {
|
|
result.error = "No address specified in URI";
|
|
return result;
|
|
}
|
|
|
|
// Address format check via the shared, structure-based recognizers (checksum + real DragonX
|
|
// address types). This accepts shielded ("zs…"), P2PKH ("R…") and P2SH/multisig ("b…") forms —
|
|
// the old prefix/length heuristic rejected P2SH and hardcoded a 't' prefix DragonX never emits.
|
|
const bool validFormat = isShieldedAddress(result.address) ||
|
|
isTransparentAddress(result.address);
|
|
|
|
if (!validFormat) {
|
|
result.error = "Invalid address format";
|
|
return result;
|
|
}
|
|
|
|
// Validate amount if present
|
|
if (!std::isfinite(result.amount) || result.amount < 0 || result.amount > 21000000.0) {
|
|
result.error = "Invalid negative amount";
|
|
return result;
|
|
}
|
|
|
|
result.valid = true;
|
|
return result;
|
|
}
|
|
|
|
} // namespace util
|
|
} // namespace dragonx
|