What changed, and why it matters
This commit adds a feature that lets users rename their wallet file from the mobile-style QML user interface. It is a normal feature addition, not a security fix. The code does include some safety checks, but there are small gaps that could theoretically allow a misnamed or clashing wallet file. There is no evidence this was released as a security patch or credited to a security researcher.
Treat as a feature commit, not a security patch. If reviewing for hardening, consider adding atomic rename, reserved-name filtering, and graceful re-load or rollback if rename_wallet_file raises. End users do not need urgent action.
Security signals we found
New file-system rename primitive exposed to QML UI
Validation of wallet name exists but is limited to non-empty, non-duplicate, basename-only checks
Race window between existence check and os.rename
Current wallet is set to None and stopped before rename; failure path leaves wallet unloaded
No explicit path traversal protection beyond os.path.basename check
Evidence from the diff
The change introduces rename_wallet_file() in Daemon, plus QML UI wiring and a QEDaemon.isValidWalletName() / renameWallet() slot. The rename path is checked for existence before os.rename, and the UI prevents empty names and exact duplicates. However, the validation only checks basename equality and existing wallet names; it does not appear to sanitize against reserved filenames, traversal via names containing path separators (already blocked by basename check), or race conditions between check and rename. The current wallet is unloaded before rename and not automatically reloaded on failure, which is a usability issue rather than a clear security flaw. No CVE, advisory, or vendor security disclosure is present in the materials.
Changed components
electrum/daemon.pyelectrum/gui/qml/qedaemon.pyelectrum/gui/qml/components/WalletDetails.qmlInspect captured patch +106 / −0
diff --git a/electrum/daemon.py b/electrum/daemon.py
index 56c5f10..db080aa 100644
--- a/electrum/daemon.py
+++ b/electrum/daemon.py
@@ -585,6 +585,17 @@ class Daemon(Logger):
return True
return False
+ def rename_wallet_file(self, old_path: str, new_path: str):
+ old_path = standardize_path(old_path)
+ new_path = standardize_path(new_path)
+ if os.path.exists(new_path):
+ raise ValueError("Wallet file already exists")
+ os.rename(old_path, new_path)
+ self.logger.debug(f'renamed wallet: {old_path} -> {new_path}')
+ self.update_recently_opened_wallets(old_path, remove=True)
+ if self.config.CURRENT_WALLET == old_path:
+ self.config.CURRENT_WALLET = new_path
+
def stop_wallet(self, path: str) -> bool:
"""Returns True iff a wallet was found."""
assert util.get_running_loop() != util.get_asyncio_loop(), 'must not be called from asyncio thread'
diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml
index 8522c42..2676c58 100644
--- a/electrum/gui/qml/components/WalletDetails.qml
+++ b/electrum/gui/qml/components/WalletDetails.qml
@@ -130,6 +130,64 @@ Pane {
visible: Daemon.currentWallet
columns: 2
+ Label {
+ Layout.columnSpan: 2
+ Layout.topMargin: constants.paddingSmall
+ text: qsTr('Name')
+ color: Material.accentColor
+ }
+
+ TextHighlightPane {
+ id: walletNameContent
+ property bool editmode: false
+ Layout.columnSpan: 2
+ Layout.fillWidth: true
+
+ RowLayout {
+ width: parent.width
+ Label {
+ visible: !walletNameContent.editmode
+ text: Daemon.currentWallet.name
+ wrapMode: Text.Wrap
+ Layout.fillWidth: true
+ font.pixelSize: constants.fontSizeLarge
+ }
+ ToolButton {
+ visible: !walletNameContent.editmode
+ icon.source: '../../icons/pen.png'
+ icon.color: 'transparent'
+ onClicked: {
+ walletNameEdit.text = Daemon.currentWallet.name
+ walletNameContent.editmode = true
+ walletNameEdit.focus = true
+ }
+ }
+ TextField {
+ id: walletNameEdit
+ visible: walletNameContent.editmode
+ Layout.fillWidth: true
+ font.pixelSize: constants.fontSizeLarge
+ }
+ ToolButton {
+ visible: walletNameContent.editmode
+ icon.source: '../../icons/confirmed.png'
+ icon.color: enabled ? 'transparent' : constants.mutedForeground
+ enabled: walletNameEdit.text !== Daemon.currentWallet.name
+ && Daemon.isValidWalletName(walletNameEdit.text)
+ onClicked: {
+ walletNameContent.editmode = false
+ Daemon.renameWallet(walletNameEdit.text)
+ }
+ }
+ ToolButton {
+ visible: walletNameContent.editmode
+ icon.source: '../../icons/closebutton.png'
+ icon.color: 'transparent'
+ onClicked: walletNameContent.editmode = false
+ }
+ }
+ }
+
Label {
Layout.columnSpan: 2
Layout.topMargin: constants.paddingSmall
@@ -526,6 +584,14 @@ Pane {
dialog.open()
}
}
+ function onWalletRenameError(message) {
+ var dialog = app.messageDialog.createObject(app, {
+ title: qsTr('Error'),
+ iconSource: Qt.resolvedUrl('../../icons/warning.png'),
+ text: message
+ })
+ dialog.open()
+ }
}
Connections {
diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py
index 2b75eb0..d9eadb3 100644
--- a/electrum/gui/qml/qedaemon.py
+++ b/electrum/gui/qml/qedaemon.py
@@ -150,6 +150,7 @@ class QEDaemon(AuthMixin, QObject):
walletRequiresPassword = pyqtSignal([str, str], arguments=['name', 'path'])
walletOpenError = pyqtSignal([str], arguments=["error"])
walletDeleteError = pyqtSignal([str, str], arguments=['code', 'message'])
+ walletRenameError = pyqtSignal([str], arguments=['message'])
def __init__(self, daemon: 'Daemon', plugins: 'Plugins', parent=None):
super().__init__(parent)
@@ -310,6 +311,34 @@ class QEDaemon(AuthMixin, QObject):
self.availableWallets.remove_wallet(path)
+ @pyqtSlot(str, result=bool)
+ def isValidWalletName(self, wallet_name: str) -> bool:
+ if not wallet_name:
+ return False
+ if self.availableWallets.wallet_name_exists(wallet_name):
+ return False
+ # ensure wallet_name is not interpreted as path
+ if os.path.basename(wallet_name) != wallet_name: # '/foo/bar/' returns 'bar'
+ return False
+ return True
+
+ @pyqtSlot(str)
+ def renameWallet(self, new_name: str):
+ wallet = self._current_wallet
+ assert wallet, "name change without wallet?"
+ old_path = standardize_path(wallet.wallet.storage.path)
+ wallet_dir = os.path.dirname(old_path)
+ new_path = standardize_path(os.path.join(wallet_dir, new_name))
+ if old_path == new_path:
+ return
+ self._current_wallet = None
+ self.daemon.stop_wallet(old_path)
+ try:
+ self.daemon.rename_wallet_file(old_path, new_path)
+ except Exception as e:
+ self.walletRenameError.emit(_('Error renaming wallet:\n') + str(e))
+ self.walletLoaded.emit(None, None)
+
@pyqtProperty(bool, notify=loadingChanged)
def loading(self):
return self._loading
Why this scored 30/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.