What changed, and why it matters
This commit upgrades a database library (jdbi) and adds a new validation step that checks a Sparrow wallet file's embedded H2 database for unexpected objects such as custom routines, triggers, check constraints, non-base tables, generated columns, and domains. It also tightens a filename check to inspect the full wallet file path. These changes look like hardening against a maliciously crafted wallet file, but the commit message only says 'upgrade jdbi to v3.51.0' and does not explain the security relevance. No advisory or CVE is referenced in the materials.
Treat as a likely security-hardening change. Review the jdbi 3.51.0 release notes and H2 changelog for relevant security fixes. Ensure validateSchema() covers all malicious object types and that the absolute-path injection check does not introduce path-handling regressions. Consider whether the new validation should also run before migration, not only after.
Security signals we found
New schema validation queries INFORMATION_SCHEMA for unexpected database objects after migration
JDBC URL injection check expanded from file name to full absolute path
jdbi upgrade from 3.49.5 to 3.51.0 may include security fixes in the dependency
Validation throws StorageException with 'This is not a valid wallet file' on unexpected schema objects
Commit message is silent on security relevance despite defensive code changes
Evidence from the diff
The diff bumps org.jdbi:jdbi3-core and jdbi3-sqlobject from 3.49.5 to 3.51.0. It also introduces validateSchema() in DbPersistence.java, invoked after migrate() for both the master schema and each child schema. validateSchema() queries INFORMATION_SCHEMA for ROUTINES, TRIGGERS, CHECK_CONSTRAINTS, non-BASE TABLE objects, GENERATION_EXPRESSION columns, and DOMAINS, throwing StorageException if any are found. Additionally, getUrl() changes the JDBC URL injection pattern check from walletFile.getName() to walletFile.getAbsolutePath(). The combination suggests defense-in-depth against H2/SQL injection or malicious schema objects in wallet files, possibly related to a known H2/jdbi issue addressed in the 3.51.0 release.
Changed components
build.gradle dependency versions for jdbi3-core and jdbi3-sqlobjectcom.sparrowwallet.sparrow.io.db.DbPersistence wallet loading/migration logicH2 database URL construction in getUrl()Wallet file schema validation routine validateSchema()Inspect captured patch +52 / −4
diff --git a/build.gradle b/build.gradle
index e310bac..a6c97ef 100644
--- a/build.gradle
+++ b/build.gradle
@@ -50,10 +50,10 @@ dependencies {
implementation('com.zaxxer:HikariCP:7.0.2') {
exclude group: 'org.slf4j'
}
- implementation('org.jdbi:jdbi3-core:3.49.5') {
+ implementation('org.jdbi:jdbi3-core:3.51.0') {
exclude group: 'org.slf4j'
}
- implementation('org.jdbi:jdbi3-sqlobject:3.49.5') {
+ implementation('org.jdbi:jdbi3-sqlobject:3.51.0') {
exclude group: 'org.slf4j'
}
implementation('org.flywaydb:flyway-core:9.22.3')
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java
index 3567491..ca42ee0 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java
@@ -83,6 +83,7 @@ public class DbPersistence implements Persistence {
ECKey encryptionKey = getEncryptionKey(password, storage.getWalletFile(), alreadyDerivedKey);
migrate(storage, MASTER_SCHEMA, encryptionKey);
+ validateSchema(storage, MASTER_SCHEMA, encryptionKey);
Jdbi jdbi = getJdbi(storage, getFilePassword(encryptionKey));
masterWallet = jdbi.withHandle(handle -> {
@@ -112,6 +113,7 @@ public class DbPersistence implements Persistence {
Map<WalletAndKey, Storage> childWallets = new TreeMap<>();
for(String schema : childSchemas) {
migrate(storage, schema, encryptionKey);
+ validateSchema(storage, schema, encryptionKey);
Jdbi childJdbi = getJdbi(storage, getFilePassword(encryptionKey));
Wallet wallet = childJdbi.withHandle(handle -> {
@@ -409,6 +411,52 @@ public class DbPersistence implements Persistence {
}
}
+ private void validateSchema(Storage storage, String schema, ECKey encryptionKey) throws StorageException {
+ Jdbi jdbi = getJdbi(storage, getFilePassword(encryptionKey));
+ try {
+ jdbi.useHandle(handle -> {
+ List<String> routines = handle.createQuery("SELECT ROUTINE_NAME FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA <> 'INFORMATION_SCHEMA'").mapTo(String.class).list();
+ if(!routines.isEmpty()) {
+ throw new RuntimeException(new StorageException("Wallet file contains unexpected database routines: " + String.join(", ", routines) + "."));
+ }
+
+ List<String> triggers = handle.createQuery("SELECT TRIGGER_NAME FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA <> 'INFORMATION_SCHEMA'").mapTo(String.class).list();
+ if(!triggers.isEmpty()) {
+ throw new RuntimeException(new StorageException("Wallet file contains unexpected database triggers: " + String.join(", ", triggers) + "."));
+ }
+
+ List<String> checkConstraints = handle.createQuery("SELECT CHECK_CLAUSE FROM INFORMATION_SCHEMA.CHECK_CONSTRAINTS WHERE UPPER(CONSTRAINT_SCHEMA) = UPPER(:schema)")
+ .bind("schema", schema).mapTo(String.class).list();
+ if(!checkConstraints.isEmpty()) {
+ throw new RuntimeException(new StorageException("Wallet file contains unexpected check constraints: " + String.join(", ", checkConstraints) + "."));
+ }
+
+ List<Map<String, Object>> nonBaseTables = handle.createQuery("SELECT TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES WHERE UPPER(TABLE_SCHEMA) = UPPER(:schema) "
+ + "AND TABLE_TYPE <> 'BASE TABLE' AND UPPER(TABLE_NAME) <> 'FLYWAY_SCHEMA_HISTORY'").bind("schema", schema).mapToMap().list();
+ if(!nonBaseTables.isEmpty()) {
+ String detail = nonBaseTables.stream().map(m -> m.get("TABLE_NAME") + " (" + m.get("TABLE_TYPE") + ")").collect(Collectors.joining(", "));
+ throw new RuntimeException(new StorageException("Wallet file contains unexpected database object types: " + detail + "."));
+ }
+
+ List<String> generatedColumns = handle.createQuery("SELECT TABLE_NAME || '.' || COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE UPPER(TABLE_SCHEMA) = UPPER(:schema) AND GENERATION_EXPRESSION IS NOT NULL")
+ .bind("schema", schema).mapTo(String.class).list();
+ if(!generatedColumns.isEmpty()) {
+ throw new RuntimeException(new StorageException("Wallet file contains unexpected generated columns: " + String.join(", ", generatedColumns) + "."));
+ }
+
+ List<String> domains = handle.createQuery("SELECT DOMAIN_NAME FROM INFORMATION_SCHEMA.DOMAINS WHERE DOMAIN_SCHEMA <> 'INFORMATION_SCHEMA'").mapTo(String.class).list();
+ if(!domains.isEmpty()) {
+ throw new RuntimeException(new StorageException("Wallet file contains unexpected database domains: " + String.join(", ", domains) + "."));
+ }
+ });
+ } catch(RuntimeException e) {
+ if(e.getCause() instanceof StorageException) {
+ throw new StorageException("This is not a valid wallet file.\n\n" + e.getCause().getMessage());
+ }
+ throw e;
+ }
+ }
+
private void cleanAndMigrate(Storage storage, String schema, String password) throws StorageException {
File migrationDir = getMigrationDir();
try {
@@ -698,8 +746,8 @@ public class DbPersistence implements Persistence {
}
private String getUrl(File walletFile, String password) throws StorageException {
- if(JDBC_URL_INJECTION_PATTERN.matcher(walletFile.getName()).find()) {
- throw new StorageException("Wallet file name contains invalid characters");
+ if(JDBC_URL_INJECTION_PATTERN.matcher(walletFile.getAbsolutePath()).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");
}
Why this scored 57/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.