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 already exists on the server. It also refactors existing filename validation into a shared helper. The change is mostly a feature addition, but it does expose filesystem information (file presence) over the RPC interface, which can aid reconnaissance if the RPC endpoint is reachable by an attacker.
Treat this as a low-sensitivity informational exposure. Ensure wallet RPC is not exposed to untrusted networks, confirm restricted mode is used where appropriate, and consider whether wallet_exists should require the same authentication level as open_wallet. No immediate patching is required unless the RPC interface is publicly reachable.
Security signals we found
New RPC endpoint exposes filesystem state (existence of specific wallet files)
Refactored path-separator validation reduces code duplication but does not strengthen it
Restricted-mode denial is present, limiting exposure on restricted RPC servers
No authentication or authorization change beyond existing RPC model
Evidence from the diff
The patch introduces COMMAND_RPC_WALLET_EXISTS and an on_wallet_exists handler. It reuses is_valid_wallet_filename (extracted from create/open/restore_wallet paths) and calls tools::wallet2::wallet_exists() to report whether the keys file and wallet cache file exist under m_wallet_dir. The command is blocked in restricted mode and requires a non-empty filename. It does not read wallet contents, only checks file existence.
Changed components
src/wallet/wallet_rpc_server.cppsrc/wallet/wallet_rpc_server.hsrc/wallet/wallet_rpc_server_commands_defs.hMonero wallet RPC serverInspect captured patch +120 / −49
diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp
index 8239aa0..0737205 100644
--- a/src/wallet/wallet_rpc_server.cpp
+++ b/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
@@ -3608,19 +3626,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))
@@ -3706,19 +3713,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
@@ -3764,6 +3760,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)
@@ -3932,19 +3955,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())
@@ -4122,19 +4134,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())
diff --git a/src/wallet/wallet_rpc_server.h b/src/wallet/wallet_rpc_server.h
index 5e87d54..fc427c9 100644
--- a/src/wallet/wallet_rpc_server.h
+++ b/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);
diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h
index 8a80dc2..c281ed2 100644
--- a/src/wallet/wallet_rpc_server_commands_defs.h
+++ b/src/wallet/wallet_rpc_server_commands_defs.h
@@ -47,7 +47,7 @@
// advance which version they will stop working with
// Don't go over 32767 for any of these
#define WALLET_RPC_VERSION_MAJOR 1
-#define WALLET_RPC_VERSION_MINOR 33
+#define WALLET_RPC_VERSION_MINOR 34
#define MAKE_WALLET_RPC_VERSION(major,minor) (((major)<<16)|(minor))
#define WALLET_RPC_VERSION MAKE_WALLET_RPC_VERSION(WALLET_RPC_VERSION_MAJOR, WALLET_RPC_VERSION_MINOR)
namespace tools
@@ -2235,6 +2235,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
diff --git a/tests/functional_tests/wallet.py b/tests/functional_tests/wallet.py
index e38cf70..f26733e 100755
--- a/tests/functional_tests/wallet.py
+++ b/tests/functional_tests/wallet.py
@@ -49,6 +49,7 @@ class WalletTest():
self.update_lookahead()
self.attributes()
self.open_close()
+ self.wallet_exists()
self.languages()
self.change_password()
self.store()
@@ -300,6 +301,37 @@ class WalletTest():
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()
diff --git a/utils/python-rpc/framework/wallet.py b/utils/python-rpc/framework/wallet.py
index 7f493d9..1fa75b5 100644
--- a/utils/python-rpc/framework/wallet.py
+++ b/utils/python-rpc/framework/wallet.py
@@ -360,6 +360,17 @@ class Wallet(object):
}
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 25/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.