wallet_rpc_server: validate seed language early
What changed, and why it matters
This change moves an error check earlier in the Monero wallet's 'generate wallet from keys' RPC command. Previously, the code would create a wallet file before checking whether the requested seed language was valid. Now it checks the language first and refuses to proceed if it's invalid. The main risk is that a user or attacker could supply a bad language and leave behind a partially-created wallet file or trigger unexpected behavior. The fix is straightforward and defensive, and the test confirms the invalid language is now rejected before any file is written.
No immediate action required beyond normal patch review and merge. The change is defensive and improves fail-fast behavior. Consider whether other wallet RPC endpoints that accept a language parameter have similar ordering issues.
Security signals we found
Input validation moved earlier in request handling
New functional test asserts no wallet files are created on invalid language
Language parameter added to RPC test helper
Evidence from the diff
In wallet_rpc_server.cpp, the language validation for generate_from_keys was relocated from after wallet creation to before it. The RPC handler now calls crypto::ElectrumWords::is_valid_language(req.language) before invoking wallet2 setup code. A functional test was added that calls generate_from_keys with language=’invalid’, expects the ‘The specified seed language is invalid.’ error, and asserts that no wallet file or keys file is created. The Python RPC helper was updated to expose the language parameter.
Changed components
src/wallet/wallet_rpc_server.cpptests/functional_tests/wallet.pyutils/python-rpc/framework/wallet.pyInspect captured patch +41 / −7
diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp
index 8239aa0..2cb3f3f 100644
--- a/src/wallet/wallet_rpc_server.cpp
+++ b/src/wallet/wallet_rpc_server.cpp
@@ -3961,6 +3961,12 @@ namespace tools
return false;
}
}
+ if (!req.language.empty() && !crypto::ElectrumWords::is_valid_language(req.language))
+ {
+ er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
+ er.message = "The specified seed language is invalid.";
+ return false;
+ }
{
po::options_description desc("dummy");
@@ -4071,12 +4077,6 @@ namespace tools
if (!req.language.empty())
{
- if (!crypto::ElectrumWords::is_valid_language(req.language))
- {
- er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
- er.message = "The specified seed language is invalid.";
- return false;
- }
wal->set_seed_language(req.language);
}
diff --git a/tests/functional_tests/wallet.py b/tests/functional_tests/wallet.py
index e38cf70..0b802cf 100755
--- a/tests/functional_tests/wallet.py
+++ b/tests/functional_tests/wallet.py
@@ -50,6 +50,7 @@ class WalletTest():
self.attributes()
self.open_close()
self.languages()
+ self.generate_from_keys()
self.change_password()
self.store()
@@ -320,6 +321,38 @@ class WalletTest():
res = wallet.query_key('mnemonic')
wallet.close_wallet()
+ def generate_from_keys(self):
+ print('Generating wallet from keys')
+ wallet = Wallet()
+ filename = 'generate-from-keys'
+ address = '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm'
+ spend_key = '148d78d2aba7dbca5cd8f6abcfb0b3c009ffbdbea1ff373d50ed94d78286640e'
+ view_key = '49774391fa5e8d249fc2c5b45dadef13534bf2483dede880dac88f061e809100'
+ 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'
+
+ try: wallet.close_wallet()
+ except: pass
+ util_resources.remove_wallet_files(filename)
+
+ ok = False
+ try:
+ wallet.generate_from_keys(filename = filename, address = address,
+ spendkey = spend_key, viewkey = view_key, language = 'invalid')
+ except Exception as e:
+ assert 'The specified seed language is invalid.' in str(e)
+ ok = True
+ assert ok
+ assert not util_resources.file_exists(filename)
+ assert not util_resources.file_exists(filename + '.keys')
+
+ res = wallet.generate_from_keys(filename = filename, address = address,
+ spendkey = spend_key, viewkey = view_key, language = 'English')
+ assert res.address == address
+ assert wallet.query_key('mnemonic').key == seed
+
+ wallet.close_wallet()
+ util_resources.remove_wallet_files(filename)
+
def change_password(self):
print('Testing password change')
wallet = Wallet()
diff --git a/utils/python-rpc/framework/wallet.py b/utils/python-rpc/framework/wallet.py
index 7f493d9..c742299 100644
--- a/utils/python-rpc/framework/wallet.py
+++ b/utils/python-rpc/framework/wallet.py
@@ -317,7 +317,7 @@ class Wallet(object):
}
return self.rpc.send_json_rpc_request(restore_deterministic_wallet)
- def generate_from_keys(self, restore_height = 0, filename = "", password = "", address = "", spendkey = "", viewkey = "", autosave_current = True):
+ def generate_from_keys(self, restore_height = 0, filename = "", password = "", address = "", spendkey = "", viewkey = "", autosave_current = True, language = ""):
generate_from_keys = {
'method': 'generate_from_keys',
'params' : {
@@ -328,6 +328,7 @@ class Wallet(object):
'viewkey': viewkey,
'password': password,
'autosave_current': autosave_current,
+ 'language': language,
},
'jsonrpc': '2.0',
'id': '0'
Why this scored 35/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.