fix(console): redact secret-bearing commands from the console echo and history (W7-1)

The RPC console echoed and stored typed commands verbatim, so `walletpassphrase
<secret>`, `z_importkey <key>`, `encryptwallet <pass>` etc. left the secret in the
visible log AND the 100-entry recall history (copyable). Adds a pure, unit-testable
RedactConsoleCommand()/ConsoleCommandCarriesSecret() (allowlist of 13 secret-bearing
first-tokens) in console_tab_helpers; submitConsoleCommand() now echoes and stores
`> walletpassphrase ****` while still executing the real command unredacted. Bare
secret commands and non-secret commands pass through unchanged.

Output-secret commands (dumpprivkey / z_exportkey / z_exportmnemonic) — whose secret is
in the RESULT — are a separate redaction concern, tracked as a follow-up.

First fix in the wallet-hardening P0-A cluster (see docs/wallet-hardening.md). New
testConsoleSecretRedaction (11 assertions); ctest 1/1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 13:57:52 -05:00
parent d188a08db7
commit da0e9f5915
5 changed files with 201 additions and 2 deletions

View File

@@ -1416,8 +1416,11 @@ bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::s
{
if (cmd.empty()) return false;
addLine("> " + cmd, ConsoleChannel::Command);
AppendConsoleHistory(command_history_, cmd, 100);
// Redact secret-bearing commands (walletpassphrase, z_importkey, …) before they reach the visible
// log and the recall history. The real `cmd` below is still executed unredacted.
const std::string display = RedactConsoleCommand(cmd);
addLine("> " + display, ConsoleChannel::Command);
AppendConsoleHistory(command_history_, display, 100);
history_index_ = -1;
// First token, lowercased, for built-in interception.

View File

@@ -1,10 +1,34 @@
#include "console_tab_helpers.h"
#include <algorithm>
#include <cctype>
namespace dragonx {
namespace ui {
namespace {
// First tokens (lowercase) of console/RPC commands that carry a secret argument on the command line.
// Output-secret commands (dumpprivkey / z_exportkey / z_exportmnemonic) are deliberately absent —
// their secret is in the RESULT, which is a separate redaction concern.
const char* const kSecretConsoleCommands[] = {
"walletpassphrase", "walletpassphrasechange", "encryptwallet",
"importprivkey", "importwallet", "importmulti",
"z_importkey", "z_importviewingkey", "z_importwallet",
"signrawtransaction", "magicrecoverkey", "sethdseed", "importmnemonic",
};
std::string firstConsoleTokenLower(const std::string& cmd, size_t& tokenEnd) {
size_t b = cmd.find_first_not_of(" \t");
if (b == std::string::npos) { tokenEnd = cmd.size(); return {}; }
size_t e = cmd.find_first_of(" \t", b);
tokenEnd = (e == std::string::npos) ? cmd.size() : e;
std::string t = cmd.substr(b, tokenEnd - b);
std::transform(t.begin(), t.end(), t.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return t;
}
} // namespace
float ComputeConsoleInputHeight(float frameHeightWithSpacing,
float itemSpacingY,
float spacingSm,
@@ -27,5 +51,27 @@ float ClampConsoleWrapWidth(float contentWidth, float paddingX)
return std::max(50.0f, contentWidth - paddingX * 2.0f);
}
bool ConsoleCommandCarriesSecret(const std::string& cmd)
{
size_t end = 0;
const std::string name = firstConsoleTokenLower(cmd, end);
if (name.empty()) return false;
for (const char* s : kSecretConsoleCommands) if (name == s) return true;
return false;
}
std::string RedactConsoleCommand(const std::string& cmd)
{
size_t end = 0;
const std::string name = firstConsoleTokenLower(cmd, end);
if (name.empty()) return cmd;
bool secret = false;
for (const char* s : kSecretConsoleCommands) if (name == s) { secret = true; break; }
if (!secret) return cmd;
// Only redact if there are actually arguments after the command name.
if (cmd.find_first_not_of(" \t", end) == std::string::npos) return cmd;
return cmd.substr(0, end) + " ****";
}
} // namespace ui
} // namespace dragonx

View File

@@ -1,5 +1,7 @@
#pragma once
#include <string>
namespace dragonx {
namespace ui {
@@ -14,5 +16,14 @@ float ComputeConsoleOutputHeight(float availableHeight,
float minHeightRatio);
float ClampConsoleWrapWidth(float contentWidth, float paddingX);
// True if `cmd`'s first token names a console/RPC command that carries a SECRET on its command line
// (passphrase, private/spending/viewing key, mnemonic). Output-secret commands (dumpprivkey,
// z_exportkey, z_exportmnemonic) are NOT covered — their secret is in the result, a separate concern.
bool ConsoleCommandCarriesSecret(const std::string& cmd);
// A display/history-safe copy of `cmd`: the command name with its arguments replaced by "****" when
// it carries a secret, else `cmd` unchanged. The real command is still executed unredacted.
std::string RedactConsoleCommand(const std::string& cmd);
} // namespace ui
} // namespace dragonx