strip only the trailing extension when deriving the h2 database name
What changed, and why it matters
This commit fixes a bug in how Sparrow Wallet names its internal H2 database files. Previously, the app stripped every occurrence of the wallet file extension from the full file path, not just the trailing one. That meant a wallet file named something like 'backup.mv.db2.mv.db' would accidentally write its database data to a different file ('backup2') instead of the file Sparrow was tracking. The fix removes only the final extension. The bug could cause wallet data to be written to or read from the wrong file, potentially leading to data loss, confusion, or cross-wallet contamination, though direct theft of funds is not evident from the change.
Treat this as a data-integrity bug with possible security side effects. Review whether any wallet could have been saved to or loaded from an unintended path, and consider warning users who created wallets with names containing the extension substring. No immediate remote exploit is visible, but users should upgrade to the fixed version to avoid data-loss or cross-wallet confusion.
Security signals we found
File path manipulation leading to wrong target file
Potential cross-wallet data contamination
Data integrity / data loss risk from mismatched tracked vs written file
JDBC URL injection pattern check already present, but extension stripping bypassed file targeting
Evidence from the diff
DbPersistence.getUrl() previously used String.replace(‘.’ + extension, ‘’) on walletFile.getAbsolutePath() to derive the H2 database URL. Because H2 itself appends ‘.mv.db’ to the supplied database name, the code removed the known wallet extension before handing the path to H2. However, replace() removes all occurrences, so if a directory or base name in the path also contained the extension string, the resulting database name pointed to a different file. The patch now uses getWalletName(dbFile, null) to strip only the trailing extension, then constructs a File from the parent directory and that base name. A regression test confirms that a wallet named ‘backup.mv.db2.mv.db’ no longer overwrites ‘backup2’.
Changed components
src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.javaH2 database URL constructionWallet file persistence layerInspect captured patch +23 / −2
### src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java
@@ -933,10 +933,15 @@ private HikariDataSource createDataSource(File walletFile, String password) thro
}
private String getUrl(File walletFile, String password) throws StorageException {
- if(JDBC_URL_INJECTION_PATTERN.matcher(walletFile.getAbsolutePath()).find()) {
+ File dbFile = walletFile.getAbsoluteFile();
+ if(JDBC_URL_INJECTION_PATTERN.matcher(dbFile.getPath()).find()) {
throw new StorageException("Wallet file path contains invalid characters");
}
- return "jdbc:h2:" + walletFile.getAbsolutePath().replace("." + getType().getExtension(), "") + ";INIT=SET TRACE_LEVEL_FILE=4;TRACE_LEVEL_FILE=4;DEFRAG_ALWAYS=true;MAX_COMPACT_TIME=5000;DATABASE_TO_UPPER=false" + (password == null ? "" : ";CIPHER=AES");
+
+ //H2 appends the extension to the database name in the URL, so only a trailing extension can be removed - removing every occurrence would
+ //open a different file to the one tracked here, as would removing one from a directory name
+ File dbName = new File(dbFile.getParentFile(), getWalletName(dbFile, null));
+ return "jdbc:h2:" + dbName.getPath() + ";INIT=SET TRACE_LEVEL_FILE=4;TRACE_LEVEL_FILE=4;DEFRAG_ALWAYS=true;MAX_COMPACT_TIME=5000;DATABASE_TO_UPPER=false" + (password == null ? "" : ";CIPHER=AES");
}
private boolean persistsFor(Wallet wallet) {
### src/test/java/com/sparrowwallet/sparrow/io/DbPersistenceTest.java
@@ -175,6 +175,22 @@ public void passwordChangeAppliesToWalletNameContainingDot() throws Exception {
Assertions.assertTrue(new Storage(PersistenceType.DB, storage.getWalletFile()).loadEncryptedWallet("pass").getWallet().isValid());
}
+ @Test
+ public void walletNameContainingExtensionUsesItsOwnFile() throws Exception {
+ Storage otherStorage = createUnencryptedWallet("backup2");
+ otherStorage.closeAndWait();
+ File otherFile = otherStorage.getWalletFile();
+ Sha256Hash otherHash = getFileHash(otherFile);
+
+ //The file for this wallet is backup.mv.db2.mv.db - removing every occurrence of the extension from the path yields the database name backup2
+ Storage storage = createUnencryptedWallet("backup.mv.db2");
+ storage.closeAndWait();
+
+ Assertions.assertTrue(storage.getWalletFile().exists(), "wallet was written to a file other than the one tracked");
+ Assertions.assertTrue(new Storage(PersistenceType.DB, storage.getWalletFile()).loadUnencryptedWallet().getWallet().isValid());
+ Assertions.assertEquals(otherHash, getFileHash(otherFile), "another wallet file was written by a wallet name containing the extension");
+ }
+
@Test
public void passwordRemovalDecryptsWalletFile() throws Exception {
Storage storage = createUnencryptedWallet("Savings");Why this scored 60/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.