What changed, and why it matters
This commit adds a new wallet RPC command called wallet_exists that lets a caller check whether a wallet file or its keys file already exists in the configured wallet directory. It also refactors existing filename validation into a shared helper. The new command is blocked in restricted RPC mode and reuses the same path-separator checks as create/open wallet, so it does not appear to introduce a new security vulnerability. It could, however, make it slightly easier for an authenticated RPC user to probe for wallet names.
Treat as a routine feature addition. Reviewers should confirm that wallet_valid_path_format() and is_valid_wallet_filename() together prevent directory traversal on all platforms, and consider rate-limiting or logging the new endpoint if wallet-name enumeration is a concern for deployments exposing RPC to multiple users.
Security signals we found
New RPC surface added (wallet_exists)
Restricted-mode denial for new command
Filename path-separator validation reused for new command
Refactoring only: no weakening of existing filename validation
Information disclosure potential: authenticated caller can enumerate wallet filenames by existence
Evidence from the diff
The patch introduces COMMAND_RPC_WALLET_EXISTS and on_wallet_exists(), registers the method in the RPC server, and adds tests/Python RPC bindings. The handler returns booleans keys_file_exists and wallet_file_exists by calling tools::wallet2::wallet_exists() on m_wallet_dir + ‘/’ + req.filename. It denies the call when m_restricted is true, when m_wallet_dir is empty, or when the filename contains ‘/’, ‘', or ‘:’ (Windows). Existing create/open/restore handlers are refactored to use a new is_valid_wallet_filename() helper, but the validation logic is unchanged. No buffer overflow, path traversal bypass, or authentication change is evident in the diff.
Changed components
src/wallet/wallet_rpc_server.cppsrc/wallet/wallet_rpc_server.hsrc/wallet/wallet_rpc_server_commands_defs.htests/functional_tests/wallet.pyutils/python-rpc/framework/wallet.pyInspect captured patch +119 / −48
### src/wallet/wallet_rpc_server.cpp
@@ -185,6 +185,24 @@ namespace
entry.suggested_confirmations_threshold = std::max(entry.suggested_confirmations_threshold, (unlock_time - now + DIFFICULTY_TARGET_V2 - 1) / DIFFICULTY_TARGET_V2);
}
}
+ //------------------------------------------------------------------------------------------------------------------------------
+ bool is_valid_wallet_filename(const std::string &filename, epee::json_rpc::error& er)
+ {
+ const char *ptr = strchr(filename.c_str(), '/');
+#ifdef _WIN32
+ if (!ptr)
+ ptr = strchr(filename.c_str(), '\\');
+ if (!ptr)
+ ptr = strchr(filename.c_str(), ':');
+#endif
+ if (ptr)
+ {
+ er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
+ er.message = "Invalid filename";
+ return false;
+ }
+ return true;
+ }
}
namespace tools
@@ -3632,19 +3650,8 @@ namespace tools
namespace po = boost::program_options;
po::variables_map vm2;
- const char *ptr = strchr(req.filename.c_str(), '/');
-#ifdef _WIN32
- if (!ptr)
- ptr = strchr(req.filename.c_str(), '\\');
- if (!ptr)
- ptr = strchr(req.filename.c_str(), ':');
-#endif
- if (ptr)
- {
- er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
- er.message = "Invalid filename";
+ if (!is_valid_wallet_filename(req.filename, er))
return false;
- }
std::string wallet_file = req.filename.empty() ? "" : (m_wallet_dir + "/" + req.filename);
{
if (!crypto::ElectrumWords::is_valid_language(req.language, req.polyseed))
@@ -3742,19 +3749,8 @@ namespace tools
namespace po = boost::program_options;
po::variables_map vm2;
- const char *ptr = strchr(req.filename.c_str(), '/');
-#ifdef _WIN32
- if (!ptr)
- ptr = strchr(req.filename.c_str(), '\\');
- if (!ptr)
- ptr = strchr(req.filename.c_str(), ':');
-#endif
- if (ptr)
- {
- er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
- er.message = "Invalid filename";
+ if (!is_valid_wallet_filename(req.filename, er))
return false;
- }
if (m_wallet && req.autosave_current)
{
try
@@ -3800,6 +3796,33 @@ namespace tools
return true;
}
//------------------------------------------------------------------------------------------------------------------------------
+ bool wallet_rpc_server::on_wallet_exists(const wallet_rpc::COMMAND_RPC_WALLET_EXISTS::request& req, wallet_rpc::COMMAND_RPC_WALLET_EXISTS::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (m_restricted)
+ {
+ er.code = WALLET_RPC_ERROR_CODE_DENIED;
+ er.message = "Command unavailable in restricted mode.";
+ return false;
+ }
+ if (m_wallet_dir.empty())
+ {
+ er.code = WALLET_RPC_ERROR_CODE_NO_WALLET_DIR;
+ er.message = "No wallet dir configured.";
+ return false;
+ }
+ if (!tools::wallet2::wallet_valid_path_format(req.filename))
+ {
+ er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
+ er.message = "Filename is required.";
+ return false;
+ }
+ if (!is_valid_wallet_filename(req.filename, er))
+ return false;
+ std::string wallet_file = m_wallet_dir + "/" + req.filename;
+ tools::wallet2::wallet_exists(wallet_file, res.keys_file_exists, res.wallet_file_exists);
+ return true;
+ }
+ //------------------------------------------------------------------------------------------------------------------------------
bool wallet_rpc_server::on_close_wallet(const wallet_rpc::COMMAND_RPC_CLOSE_WALLET::request& req, wallet_rpc::COMMAND_RPC_CLOSE_WALLET::response& res, epee::json_rpc::error& er, const connection_context *ctx)
{
if (m_restricted)
@@ -3968,19 +3991,8 @@ namespace tools
namespace po = boost::program_options;
po::variables_map vm2;
- const char *ptr = strchr(req.filename.c_str(), '/');
- #ifdef _WIN32
- if (!ptr)
- ptr = strchr(req.filename.c_str(), '\\');
- if (!ptr)
- ptr = strchr(req.filename.c_str(), ':');
- #endif
- if (ptr)
- {
- er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
- er.message = "Invalid filename";
+ if (!is_valid_wallet_filename(req.filename, er))
return false;
- }
std::string wallet_file = req.filename.empty() ? "" : (m_wallet_dir + "/" + req.filename);
// check if wallet file already exists
if (!wallet_file.empty())
@@ -4157,19 +4169,8 @@ namespace tools
namespace po = boost::program_options;
po::variables_map vm2;
- const char *ptr = strchr(req.filename.c_str(), '/');
- #ifdef _WIN32
- if (!ptr)
- ptr = strchr(req.filename.c_str(), '\\');
- if (!ptr)
- ptr = strchr(req.filename.c_str(), ':');
- #endif
- if (ptr)
- {
- er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
- er.message = "Invalid filename";
+ if (!is_valid_wallet_filename(req.filename, er))
return false;
- }
std::string wallet_file = req.filename.empty() ? "" : (m_wallet_dir + "/" + req.filename);
// check if wallet file already exists
if (!wallet_file.empty())
### src/wallet/wallet_rpc_server.h
@@ -143,6 +143,7 @@ namespace tools
MAP_JON_RPC_WE("get_languages", on_get_languages, wallet_rpc::COMMAND_RPC_GET_LANGUAGES)
MAP_JON_RPC_WE("create_wallet", on_create_wallet, wallet_rpc::COMMAND_RPC_CREATE_WALLET)
MAP_JON_RPC_WE("open_wallet", on_open_wallet, wallet_rpc::COMMAND_RPC_OPEN_WALLET)
+ MAP_JON_RPC_WE("wallet_exists", on_wallet_exists, wallet_rpc::COMMAND_RPC_WALLET_EXISTS)
MAP_JON_RPC_WE("close_wallet", on_close_wallet, wallet_rpc::COMMAND_RPC_CLOSE_WALLET)
MAP_JON_RPC_WE("change_wallet_password", on_change_wallet_password, wallet_rpc::COMMAND_RPC_CHANGE_WALLET_PASSWORD)
MAP_JON_RPC_WE("generate_from_keys", on_generate_from_keys, wallet_rpc::COMMAND_RPC_GENERATE_FROM_KEYS)
@@ -239,6 +240,7 @@ namespace tools
bool on_get_languages(const wallet_rpc::COMMAND_RPC_GET_LANGUAGES::request& req, wallet_rpc::COMMAND_RPC_GET_LANGUAGES::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
bool on_create_wallet(const wallet_rpc::COMMAND_RPC_CREATE_WALLET::request& req, wallet_rpc::COMMAND_RPC_CREATE_WALLET::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
bool on_open_wallet(const wallet_rpc::COMMAND_RPC_OPEN_WALLET::request& req, wallet_rpc::COMMAND_RPC_OPEN_WALLET::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
+ bool on_wallet_exists(const wallet_rpc::COMMAND_RPC_WALLET_EXISTS::request& req, wallet_rpc::COMMAND_RPC_WALLET_EXISTS::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
bool on_close_wallet(const wallet_rpc::COMMAND_RPC_CLOSE_WALLET::request& req, wallet_rpc::COMMAND_RPC_CLOSE_WALLET::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
bool on_change_wallet_password(const wallet_rpc::COMMAND_RPC_CHANGE_WALLET_PASSWORD::request& req, wallet_rpc::COMMAND_RPC_CHANGE_WALLET_PASSWORD::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
bool on_generate_from_keys(const wallet_rpc::COMMAND_RPC_GENERATE_FROM_KEYS::request& req, wallet_rpc::COMMAND_RPC_GENERATE_FROM_KEYS::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
### src/wallet/wallet_rpc_server_commands_defs.h
@@ -2246,6 +2246,31 @@ namespace wallet_rpc
typedef epee::misc_utils::struct_init<response_t> response;
};
+ struct COMMAND_RPC_WALLET_EXISTS
+ {
+ struct request_t
+ {
+ std::string filename;
+
+ BEGIN_KV_SERIALIZE_MAP()
+ KV_SERIALIZE(filename)
+ END_KV_SERIALIZE_MAP()
+ };
+ typedef epee::misc_utils::struct_init<request_t> request;
+
+ struct response_t
+ {
+ bool keys_file_exists;
+ bool wallet_file_exists;
+
+ BEGIN_KV_SERIALIZE_MAP()
+ KV_SERIALIZE(keys_file_exists)
+ KV_SERIALIZE(wallet_file_exists)
+ END_KV_SERIALIZE_MAP()
+ };
+ typedef epee::misc_utils::struct_init<response_t> response;
+ };
+
struct COMMAND_RPC_CLOSE_WALLET
{
struct request_t
### tests/functional_tests/wallet.py
@@ -49,6 +49,7 @@ def run_test(self):
self.update_lookahead()
self.attributes()
self.open_close()
+ self.wallet_exists()
self.languages()
self.generate_from_keys()
self.change_password()
@@ -331,6 +332,37 @@ def open_close(self):
res = wallet.get_address()
assert res.address == '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm'
+ def wallet_exists(self):
+ print('Testing wallet_exists')
+ wallet = Wallet()
+
+ try: wallet.close_wallet()
+ except: pass
+
+ util_resources.remove_wallet_files('test1')
+
+ res = wallet.wallet_exists('test1')
+ assert not res.keys_file_exists
+ assert not res.wallet_file_exists
+
+ seed = 'velvet lymph giddy number token physics poetry unquoted nibs useful sabotage limits benches lifestyle eden nitrogen anvil fewest avoid batch vials washing fences goat unquoted'
+ res = wallet.restore_deterministic_wallet(seed = seed, filename = 'test1')
+ assert res.address == '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm'
+ assert res.seed == seed
+
+ util_resources.remove_file('test1')
+ res = wallet.wallet_exists('test1')
+ assert res.keys_file_exists
+ assert not res.wallet_file_exists
+
+ wallet.store()
+ res = wallet.wallet_exists('test1')
+ assert res.keys_file_exists
+ assert res.wallet_file_exists
+
+ wallet.close_wallet()
+ util_resources.remove_wallet_files('test1')
+
def languages(self):
print('Testing languages')
wallet = Wallet()
### utils/python-rpc/framework/wallet.py
@@ -361,6 +361,17 @@ def open_wallet(self, filename, password='', autosave_current = True):
}
return self.rpc.send_json_rpc_request(open_wallet)
+ def wallet_exists(self, filename):
+ wallet_exists = {
+ 'method': 'wallet_exists',
+ 'params' : {
+ 'filename': filename,
+ },
+ 'jsonrpc': '2.0',
+ 'id': '0'
+ }
+ return self.rpc.send_json_rpc_request(wallet_exists)
+
def close_wallet(self, autosave_current = True):
close_wallet = {
'method': 'close_wallet',Why this scored 24/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.