From df564498231714ea26a39e267dce0c37bb5bf3ed Mon Sep 17 00:00:00 2001 From: mfw78 Date: Tue, 7 Apr 2026 11:05:08 +0000 Subject: [PATCH 1/2] 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 93f2e4aead2cacb65934f4e06386fa31ed70d681 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Wed, 8 Apr 2026 09:20:55 +0000 Subject: [PATCH 2/2] 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))]