From d7e1a66be98ed9aca081ac54ffde3b5ba3503d31 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Tue, 7 Apr 2026 11:05:08 +0000 Subject: [PATCH 1/3] Panic on unrecognised DATABASE_URL instead of silent SQLite fallback Previously, any DATABASE_URL that did not match the mysql: or postgresql: prefix was silently treated as a SQLite file path. This caused data loss in containerised environments when the URL was misconfigured (typos, quoting issues), as vaultwarden would create an ephemeral SQLite database that was wiped on restart. Now, an explicit sqlite:// prefix is supported and used as the default. Bare paths without a recognised scheme are still accepted for backwards compatibility, but only if the database file already exists. If not, the process panics with a clear error message. Relates to #2835, #1910, #860. --- .env.template | 9 +++++---- src/config.rs | 21 ++++++++++++--------- src/db/mod.rs | 34 +++++++++++++++++++++++++++------- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/.env.template b/.env.template index 03990820..7468e158 100644 --- a/.env.template +++ b/.env.template @@ -50,10 +50,11 @@ ######################### ## Database URL -## When using SQLite, this is the path to the DB file, and it defaults to -## %DATA_FOLDER%/db.sqlite3. If DATA_FOLDER is set to an external location, this -## must be set to a local sqlite3 file path. -# DATABASE_URL=data/db.sqlite3 +## When using SQLite, this should use the sqlite:// prefix followed by the path +## to the DB file. It defaults to sqlite://%DATA_FOLDER%/db.sqlite3. +## Bare paths without the sqlite:// prefix are supported for backwards compatibility, +## but only if the database file already exists. +# DATABASE_URL=sqlite://data/db.sqlite3 ## When using MySQL, specify an appropriate connection URI. ## Details: https://docs.diesel.rs/2.1.x/diesel/mysql/struct.MysqlConnection.html # DATABASE_URL=mysql://user:password@host[:port]/database_name diff --git a/src/config.rs b/src/config.rs index 6ff09467..d2f897a2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -507,7 +507,7 @@ make_config! { /// Data folder |> Main data folder data_folder: String, false, def, "data".to_string(); /// Database URL - database_url: String, false, auto, |c| format!("{}/db.sqlite3", c.data_folder); + database_url: String, false, auto, |c| format!("sqlite://{}/db.sqlite3", c.data_folder); /// Icon cache folder icon_cache_folder: String, false, auto, |c| format!("{}/icon_cache", c.data_folder); /// Attachments folder @@ -929,14 +929,17 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { { use crate::db::DbConnType; let url = &cfg.database_url; - if DbConnType::from_url(url)? == DbConnType::Sqlite && url.contains('/') { - let path = std::path::Path::new(&url); - if let Some(parent) = path.parent() { - if !parent.is_dir() { - err!(format!( - "SQLite database directory `{}` does not exist or is not a directory", - parent.display() - )); + if DbConnType::from_url(url)? == DbConnType::Sqlite { + let file_path = url.strip_prefix("sqlite://").unwrap_or(url); + if file_path.contains('/') { + let path = std::path::Path::new(file_path); + if let Some(parent) = path.parent() { + if !parent.is_dir() { + err!(format!( + "SQLite database directory `{}` does not exist or is not a directory", + parent.display() + )); + } } } } diff --git a/src/db/mod.rs b/src/db/mod.rs index d2ed9479..89c680ba 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -272,13 +272,32 @@ impl DbConnType { #[cfg(not(postgresql))] err!("`DATABASE_URL` is a PostgreSQL URL, but the 'postgresql' feature is not enabled") - //Sqlite - } else { + // Sqlite (explicit) + } else if url.len() > 7 && &url[..7] == "sqlite:" { #[cfg(sqlite)] return Ok(DbConnType::Sqlite); #[cfg(not(sqlite))] - err!("`DATABASE_URL` looks like a SQLite URL, but 'sqlite' feature is not enabled") + err!("`DATABASE_URL` is a SQLite URL, but the 'sqlite' feature is not enabled") + + // No recognized scheme — assume legacy bare-path SQLite, but the database file must already exist. + // This prevents misconfigured URLs (typos, quoted strings) from silently creating a new empty SQLite database. + } else { + #[cfg(sqlite)] + { + if std::path::Path::new(url).exists() { + return Ok(DbConnType::Sqlite); + } + panic!( + "`DATABASE_URL` does not match any known database scheme (mysql://, postgresql://, sqlite://) \ + and no existing SQLite database was found at '{url}'. \ + If you intend to use SQLite, use an explicit `sqlite://` prefix in your `DATABASE_URL`. \ + Otherwise, check your DATABASE_URL for typos or quoting issues." + ); + } + + #[cfg(not(sqlite))] + err!("`DATABASE_URL` does not match any known database scheme (mysql://, postgresql://, sqlite://)") } } @@ -390,11 +409,12 @@ pub fn backup_sqlite() -> Result { let db_url = CONFIG.database_url(); if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::Sqlite).unwrap_or(false) { - // Since we do not allow any schema for sqlite database_url's like `file:` or `sqlite:` to be set, we can assume here it isn't - // This way we can set a readonly flag on the opening mode without issues. - let mut conn = diesel::sqlite::SqliteConnection::establish(&format!("sqlite://{db_url}?mode=ro"))?; + // Strip the sqlite:// prefix if present to get the raw file path + let file_path = db_url.strip_prefix("sqlite://").unwrap_or(&db_url); + // Open a read-only connection for the backup + let mut conn = diesel::sqlite::SqliteConnection::establish(&format!("sqlite://{file_path}?mode=ro"))?; - let db_path = std::path::Path::new(&db_url).parent().unwrap(); + let db_path = std::path::Path::new(file_path).parent().unwrap(); let backup_file = db_path .join(format!("db_{}.sqlite3", chrono::Utc::now().format("%Y%m%d_%H%M%S"))) .to_string_lossy() From 44d79406b70e83d96cf8f81830623c3bfc7f65b7 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Wed, 8 Apr 2026 09:20:55 +0000 Subject: [PATCH 2/3] Use err!() instead of panic!() for unrecognised DATABASE_URL Follow the established codebase convention where configuration validation errors use err!() to propagate gracefully, rather than panic!(). The error propagates through from_config() and is caught by create_db_pool() which logs and calls exit(1). --- src/db/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/db/mod.rs b/src/db/mod.rs index 89c680ba..7b989e44 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -288,12 +288,12 @@ impl DbConnType { if std::path::Path::new(url).exists() { return Ok(DbConnType::Sqlite); } - panic!( + err!(format!( "`DATABASE_URL` does not match any known database scheme (mysql://, postgresql://, sqlite://) \ and no existing SQLite database was found at '{url}'. \ If you intend to use SQLite, use an explicit `sqlite://` prefix in your `DATABASE_URL`. \ Otherwise, check your DATABASE_URL for typos or quoting issues." - ); + )) } #[cfg(not(sqlite))] From 5da1d4099401ca371246443ea20b5c6f88cae25f Mon Sep 17 00:00:00 2001 From: mfw78 Date: Fri, 10 Apr 2026 08:30:11 +0000 Subject: [PATCH 3/3] Use 'scheme' instead of 'prefix' in DATABASE_URL messages Per review feedback, 'scheme' is the more accurate term for the sqlite:// portion of the URL. --- .env.template | 4 ++-- src/db/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.env.template b/.env.template index 7468e158..a12559ad 100644 --- a/.env.template +++ b/.env.template @@ -50,9 +50,9 @@ ######################### ## Database URL -## When using SQLite, this should use the sqlite:// prefix followed by the path +## When using SQLite, this should use the sqlite:// scheme followed by the path ## to the DB file. It defaults to sqlite://%DATA_FOLDER%/db.sqlite3. -## Bare paths without the sqlite:// prefix are supported for backwards compatibility, +## Bare paths without the sqlite:// scheme are supported for backwards compatibility, ## but only if the database file already exists. # DATABASE_URL=sqlite://data/db.sqlite3 ## When using MySQL, specify an appropriate connection URI. diff --git a/src/db/mod.rs b/src/db/mod.rs index 7b989e44..4aafe995 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -291,7 +291,7 @@ impl DbConnType { err!(format!( "`DATABASE_URL` does not match any known database scheme (mysql://, postgresql://, sqlite://) \ and no existing SQLite database was found at '{url}'. \ - If you intend to use SQLite, use an explicit `sqlite://` prefix in your `DATABASE_URL`. \ + If you intend to use SQLite, use an explicit `sqlite://` scheme in your `DATABASE_URL`. \ Otherwise, check your DATABASE_URL for typos or quoting issues." )) }