Skip to content

Commit 6dc4c43

Browse files
committed
Merge #8877: Qt RPC console: history sensitive-data filter, and saving input line when browsing history
8562792 GUI/RPCConsole: Include importmulti in history sensitive-command filter (Luke Dashjr) ff77faf Qt/RPCConsole: Use RPCParseCommandLine to perform command filtering (Luke Dashjr) a79598d Qt/Test: Make sure filtering sensitive data works correctly in nested commands (Luke Dashjr) 629cd42 Qt/RPCConsole: Teach RPCParseCommandLine how to filter out arguments to sensitive commands (Luke Dashjr) e2d9213 Qt/RPCConsole: Make it possible to parse a command without executing it (Luke Dashjr) 1755c04 Qt/RPCConsole: Truncate filtered commands to just the command name, rather than skip it entirely in history (Luke Dashjr) d80a006 Qt/RPCConsole: Add signmessagewithprivkey to list of commands filtered from history (Luke Dashjr) afde12f Qt/RPCConsole: Refactor command_may_contain_sensitive_data function out of RPCConsole::on_lineEdit_returnPressed (Luke Dashjr) de8980d Bugfix: Do not add sensitive information to history for real (Luke Dashjr) 9044908 Qt/RPCConsole: Don't store commands with potentially sensitive information in the history (Jonas Schnelli) fc95daa Qt/RPCConsole: Save current command entry when browsing history (Jonas Schnelli)
2 parents e0dc3d7 + 8562792 commit 6dc4c43

File tree

3 files changed

+131
-20
lines changed

3 files changed

+131
-20
lines changed

src/qt/rpcconsole.cpp

Lines changed: 100 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
#include <QKeyEvent>
3333
#include <QMenu>
34+
#include <QMessageBox>
3435
#include <QScrollBar>
3536
#include <QSettings>
3637
#include <QSignalMapper>
@@ -63,6 +64,20 @@ const struct {
6364
{NULL, NULL}
6465
};
6566

67+
namespace {
68+
69+
// don't add private key handling cmd's to the history
70+
const QStringList historyFilter = QStringList()
71+
<< "importprivkey"
72+
<< "importmulti"
73+
<< "signmessagewithprivkey"
74+
<< "signrawtransaction"
75+
<< "walletpassphrase"
76+
<< "walletpassphrasechange"
77+
<< "encryptwallet";
78+
79+
}
80+
6681
/* Object for executing console RPC commands in a separate thread.
6782
*/
6883
class RPCExecutor : public QObject
@@ -113,10 +128,10 @@ class QtRPCTimerInterface: public RPCTimerInterface
113128
#include "rpcconsole.moc"
114129

115130
/**
116-
* Split shell command line into a list of arguments and execute the command(s).
131+
* Split shell command line into a list of arguments and optionally execute the command(s).
117132
* Aims to emulate \c bash and friends.
118133
*
119-
* - Command nesting is possible with brackets [example: validateaddress(getnewaddress())]
134+
* - Command nesting is possible with parenthesis; for example: validateaddress(getnewaddress())
120135
* - Arguments are delimited with whitespace or comma
121136
* - Extra whitespace at the beginning and end and between arguments will be ignored
122137
* - Text can be "double" or 'single' quoted
@@ -127,9 +142,11 @@ class QtRPCTimerInterface: public RPCTimerInterface
127142
*
128143
* @param[out] result stringified Result from the executed command(chain)
129144
* @param[in] strCommand Command line to split
145+
* @param[in] fExecute set true if you want the command to be executed
146+
* @param[out] pstrFilteredOut Command line, filtered to remove any sensitive data
130147
*/
131148

132-
bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string &strCommand)
149+
bool RPCConsole::RPCParseCommandLine(std::string &strResult, const std::string &strCommand, const bool fExecute, std::string * const pstrFilteredOut)
133150
{
134151
std::vector< std::vector<std::string> > stack;
135152
stack.push_back(std::vector<std::string>());
@@ -149,12 +166,35 @@ bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string
149166
} state = STATE_EATING_SPACES;
150167
std::string curarg;
151168
UniValue lastResult;
169+
unsigned nDepthInsideSensitive = 0;
170+
size_t filter_begin_pos = 0, chpos;
171+
std::vector<std::pair<size_t, size_t>> filter_ranges;
172+
173+
auto add_to_current_stack = [&](const std::string& curarg) {
174+
if (stack.back().empty() && (!nDepthInsideSensitive) && historyFilter.contains(QString::fromStdString(curarg), Qt::CaseInsensitive)) {
175+
nDepthInsideSensitive = 1;
176+
filter_begin_pos = chpos;
177+
}
178+
stack.back().push_back(curarg);
179+
};
180+
181+
auto close_out_params = [&]() {
182+
if (nDepthInsideSensitive) {
183+
if (!--nDepthInsideSensitive) {
184+
assert(filter_begin_pos);
185+
filter_ranges.push_back(std::make_pair(filter_begin_pos, chpos));
186+
filter_begin_pos = 0;
187+
}
188+
}
189+
stack.pop_back();
190+
};
152191

153192
std::string strCommandTerminated = strCommand;
154193
if (strCommandTerminated.back() != '\n')
155194
strCommandTerminated += "\n";
156-
for(char ch: strCommandTerminated)
195+
for (chpos = 0; chpos < strCommandTerminated.size(); ++chpos)
157196
{
197+
char ch = strCommandTerminated[chpos];
158198
switch(state)
159199
{
160200
case STATE_COMMAND_EXECUTED_INNER:
@@ -173,7 +213,7 @@ bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string
173213
curarg += ch;
174214
break;
175215
}
176-
if (curarg.size())
216+
if (curarg.size() && fExecute)
177217
{
178218
// if we have a value query, query arrays with index and objects with a string key
179219
UniValue subelement;
@@ -198,7 +238,7 @@ bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string
198238
breakParsing = false;
199239

200240
// pop the stack and return the result to the current command arguments
201-
stack.pop_back();
241+
close_out_params();
202242

203243
// don't stringify the json in case of a string to avoid doublequotes
204244
if (lastResult.isStr())
@@ -210,7 +250,7 @@ bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string
210250
if (curarg.size())
211251
{
212252
if (stack.size())
213-
stack.back().push_back(curarg);
253+
add_to_current_stack(curarg);
214254
else
215255
strResult = curarg;
216256
}
@@ -236,25 +276,31 @@ bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string
236276
if (state == STATE_ARGUMENT)
237277
{
238278
if (ch == '(' && stack.size() && stack.back().size() > 0)
279+
{
280+
if (nDepthInsideSensitive) {
281+
++nDepthInsideSensitive;
282+
}
239283
stack.push_back(std::vector<std::string>());
284+
}
240285

241286
// don't allow commands after executed commands on baselevel
242287
if (!stack.size())
243288
throw std::runtime_error("Invalid Syntax");
244289

245-
stack.back().push_back(curarg);
290+
add_to_current_stack(curarg);
246291
curarg.clear();
247292
state = STATE_EATING_SPACES_IN_BRACKETS;
248293
}
249294
if ((ch == ')' || ch == '\n') && stack.size() > 0)
250295
{
251-
std::string strPrint;
252-
// Convert argument list to JSON objects in method-dependent way,
253-
// and pass it along with the method name to the dispatcher.
254-
JSONRPCRequest req;
255-
req.params = RPCConvertValues(stack.back()[0], std::vector<std::string>(stack.back().begin() + 1, stack.back().end()));
256-
req.strMethod = stack.back()[0];
257-
lastResult = tableRPC.execute(req);
296+
if (fExecute) {
297+
// Convert argument list to JSON objects in method-dependent way,
298+
// and pass it along with the method name to the dispatcher.
299+
JSONRPCRequest req;
300+
req.params = RPCConvertValues(stack.back()[0], std::vector<std::string>(stack.back().begin() + 1, stack.back().end()));
301+
req.strMethod = stack.back()[0];
302+
lastResult = tableRPC.execute(req);
303+
}
258304

259305
state = STATE_COMMAND_EXECUTED;
260306
curarg.clear();
@@ -266,7 +312,7 @@ bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string
266312

267313
else if(state == STATE_ARGUMENT) // Space ends argument
268314
{
269-
stack.back().push_back(curarg);
315+
add_to_current_stack(curarg);
270316
curarg.clear();
271317
}
272318
if ((state == STATE_EATING_SPACES_IN_BRACKETS || state == STATE_ARGUMENT) && ch == ',')
@@ -303,6 +349,16 @@ bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string
303349
break;
304350
}
305351
}
352+
if (pstrFilteredOut) {
353+
if (STATE_COMMAND_EXECUTED == state) {
354+
assert(!stack.empty());
355+
close_out_params();
356+
}
357+
*pstrFilteredOut = strCommand;
358+
for (auto i = filter_ranges.rbegin(); i != filter_ranges.rend(); ++i) {
359+
pstrFilteredOut->replace(i->first, i->second - i->first, "(…)");
360+
}
361+
}
306362
switch(state) // final state
307363
{
308364
case STATE_COMMAND_EXECUTED:
@@ -747,12 +803,30 @@ void RPCConsole::setMempoolSize(long numberOfTxs, size_t dynUsage)
747803
void RPCConsole::on_lineEdit_returnPressed()
748804
{
749805
QString cmd = ui->lineEdit->text();
750-
ui->lineEdit->clear();
751806

752807
if(!cmd.isEmpty())
753808
{
809+
std::string strFilteredCmd;
810+
try {
811+
std::string dummy;
812+
if (!RPCParseCommandLine(dummy, cmd.toStdString(), false, &strFilteredCmd)) {
813+
// Failed to parse command, so we cannot even filter it for the history
814+
throw std::runtime_error("Invalid command line");
815+
}
816+
} catch (const std::exception& e) {
817+
QMessageBox::critical(this, "Error", QString("Error: ") + QString::fromStdString(e.what()));
818+
return;
819+
}
820+
821+
ui->lineEdit->clear();
822+
823+
cmdBeforeBrowsing = QString();
824+
754825
message(CMD_REQUEST, cmd);
755826
Q_EMIT cmdRequest(cmd);
827+
828+
cmd = QString::fromStdString(strFilteredCmd);
829+
756830
// Remove command, if already in history
757831
history.removeOne(cmd);
758832
// Append command to history
@@ -762,13 +836,19 @@ void RPCConsole::on_lineEdit_returnPressed()
762836
history.removeFirst();
763837
// Set pointer to end of history
764838
historyPtr = history.size();
839+
765840
// Scroll console view to end
766841
scrollToEnd();
767842
}
768843
}
769844

770845
void RPCConsole::browseHistory(int offset)
771846
{
847+
// store current text when start browsing through the history
848+
if (historyPtr == history.size()) {
849+
cmdBeforeBrowsing = ui->lineEdit->text();
850+
}
851+
772852
historyPtr += offset;
773853
if(historyPtr < 0)
774854
historyPtr = 0;
@@ -777,6 +857,9 @@ void RPCConsole::browseHistory(int offset)
777857
QString cmd;
778858
if(historyPtr < history.size())
779859
cmd = history.at(historyPtr);
860+
else if (!cmdBeforeBrowsing.isNull()) {
861+
cmd = cmdBeforeBrowsing;
862+
}
780863
ui->lineEdit->setText(cmd);
781864
}
782865

src/qt/rpcconsole.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ class RPCConsole: public QWidget
3636
explicit RPCConsole(const PlatformStyle *platformStyle, QWidget *parent);
3737
~RPCConsole();
3838

39-
static bool RPCExecuteCommandLine(std::string &strResult, const std::string &strCommand);
39+
static bool RPCParseCommandLine(std::string &strResult, const std::string &strCommand, bool fExecute, std::string * const pstrFilteredOut = NULL);
40+
static bool RPCExecuteCommandLine(std::string &strResult, const std::string &strCommand, std::string * const pstrFilteredOut = NULL) {
41+
return RPCParseCommandLine(strResult, strCommand, true, pstrFilteredOut);
42+
}
4043

4144
void setClientModel(ClientModel *model);
4245

@@ -140,6 +143,7 @@ public Q_SLOTS:
140143
ClientModel *clientModel;
141144
QStringList history;
142145
int historyPtr;
146+
QString cmdBeforeBrowsing;
143147
QList<NodeId> cachedNodeids;
144148
const PlatformStyle *platformStyle;
145149
RPCTimerInterface *rpcTimerInterface;

src/qt/test/rpcnestedtests.cpp

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ void RPCNestedTests::rpcNestedTests()
6161

6262
std::string result;
6363
std::string result2;
64-
RPCConsole::RPCExecuteCommandLine(result, "getblockchaininfo()[chain]"); //simple result filtering with path
64+
std::string filtered;
65+
RPCConsole::RPCExecuteCommandLine(result, "getblockchaininfo()[chain]", &filtered); //simple result filtering with path
6566
QVERIFY(result=="main");
67+
QVERIFY(filtered == "getblockchaininfo()[chain]");
6668

6769
RPCConsole::RPCExecuteCommandLine(result, "getblock(getbestblockhash())"); //simple 2 level nesting
6870
RPCConsole::RPCExecuteCommandLine(result, "getblock(getblock(getbestblockhash())[hash], true)");
@@ -87,8 +89,30 @@ void RPCNestedTests::rpcNestedTests()
8789
(RPCConsole::RPCExecuteCommandLine(result2, "createrawtransaction( [], {} , 0 )")); //whitespace between parametres is allowed
8890
QVERIFY(result == result2);
8991

90-
RPCConsole::RPCExecuteCommandLine(result, "getblock(getbestblockhash())[tx][0]");
92+
RPCConsole::RPCExecuteCommandLine(result, "getblock(getbestblockhash())[tx][0]", &filtered);
9193
QVERIFY(result == "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b");
94+
QVERIFY(filtered == "getblock(getbestblockhash())[tx][0]");
95+
96+
RPCConsole::RPCParseCommandLine(result, "importprivkey", false, &filtered);
97+
QVERIFY(filtered == "importprivkey(…)");
98+
RPCConsole::RPCParseCommandLine(result, "signmessagewithprivkey abc", false, &filtered);
99+
QVERIFY(filtered == "signmessagewithprivkey(…)");
100+
RPCConsole::RPCParseCommandLine(result, "signmessagewithprivkey abc,def", false, &filtered);
101+
QVERIFY(filtered == "signmessagewithprivkey(…)");
102+
RPCConsole::RPCParseCommandLine(result, "signrawtransaction(abc)", false, &filtered);
103+
QVERIFY(filtered == "signrawtransaction(…)");
104+
RPCConsole::RPCParseCommandLine(result, "walletpassphrase(help())", false, &filtered);
105+
QVERIFY(filtered == "walletpassphrase(…)");
106+
RPCConsole::RPCParseCommandLine(result, "walletpassphrasechange(help(walletpassphrasechange(abc)))", false, &filtered);
107+
QVERIFY(filtered == "walletpassphrasechange(…)");
108+
RPCConsole::RPCParseCommandLine(result, "help(encryptwallet(abc, def))", false, &filtered);
109+
QVERIFY(filtered == "help(encryptwallet(…))");
110+
RPCConsole::RPCParseCommandLine(result, "help(importprivkey())", false, &filtered);
111+
QVERIFY(filtered == "help(importprivkey(…))");
112+
RPCConsole::RPCParseCommandLine(result, "help(importprivkey(help()))", false, &filtered);
113+
QVERIFY(filtered == "help(importprivkey(…))");
114+
RPCConsole::RPCParseCommandLine(result, "help(importprivkey(abc), walletpassphrase(def))", false, &filtered);
115+
QVERIFY(filtered == "help(importprivkey(…), walletpassphrase(…))");
92116

93117
RPCConsole::RPCExecuteCommandLine(result, "rpcNestedTest");
94118
QVERIFY(result == "[]");

0 commit comments

Comments
 (0)