From dc0e4f20f4a61acc98a644909e650564e2684b68 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Wed, 14 Oct 2026 17:44:03 +0200 Subject: [PATCH] Fix several possible panics and overflows Adjust several integer types to be a lower or unsigned type. Most types were too large, only because the function which used this value needed that specific type. But, this allowed too large values which either caused panics or might cause a DoS. This does however might possible cause configurations which have larger values, which are probably way too large anyway to be not loaded or use their defaults. During the config loading, either via env or json, it will show error logs regarding these items. Also fixed an issue with protected actions via OTP, it increased the attempt before it validated if the attempt limit was reached. This could cause an issue when set to `1`, that way it was never allowed, and `3` times would actually be `2`. Also added several config validation checks which otherwise would have prevent Vaultwarden to start, or login, so these will probably not cause issues, else users would already have issues. Signed-off-by: BlackDex --- .env.template | 6 +- src/api/admin.rs | 7 +- src/api/core/events.rs | 4 +- src/api/core/two_factor/email.rs | 4 +- src/api/core/two_factor/mod.rs | 2 +- src/api/core/two_factor/protected_actions.rs | 9 +- src/api/icons.rs | 6 +- src/api/identity.rs | 4 +- src/auth.rs | 66 ++++++---- src/config.rs | 122 ++++++++++++------- src/db/mod.rs | 6 +- src/db/models/cipher.rs | 29 +++-- src/db/models/event.rs | 4 +- src/mail.rs | 12 +- src/main.rs | 6 +- src/ratelimit.rs | 12 +- src/sso_client.rs | 2 +- src/util.rs | 18 ++- 18 files changed, 194 insertions(+), 125 deletions(-) diff --git a/.env.template b/.env.template index 272c7738..c557e826 100644 --- a/.env.template +++ b/.env.template @@ -78,7 +78,7 @@ # DB_CONNECTION_RETRIES=15 ## Database timeout -## Timeout when acquiring database connection +## Timeout in seconds when acquiring database connection # DATABASE_TIMEOUT=30 ## Database idle timeout @@ -173,7 +173,7 @@ ## Defaults to daily. Set blank to disable this job. Also without EVENTS_DAYS_RETAIN set, this job will not start. # EVENT_CLEANUP_SCHEDULE="0 10 0 * * *" ## Number of days to retain events stored in the database. -## If unset (the default), events are kept indefinitely and the scheduled job is disabled! +## If unset (the default) or 0, events are kept indefinitely and the scheduled job is disabled! # EVENTS_DAYS_RETAIN= ## ## Cron schedule of the job that cleans old auth requests from the auth request. @@ -233,7 +233,7 @@ # USER_SEND_LIMIT= ## Number of days to wait before auto-deleting a trashed item. -## If unset (the default), trashed items are not auto-deleted. +## If unset (the default) or 0, trashed items are not auto-deleted. ## This setting applies globally, so make sure to inform all users of any changes to this setting. # TRASH_AUTO_DELETE_DAYS= diff --git a/src/api/admin.rs b/src/api/admin.rs index 2a84f611..966be30e 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -201,12 +201,15 @@ fn post_admin_login( // If the token is invalid, redirect to login page if validate_token(&data.token) { // If the token received is valid, generate JWT and save it as a cookie - let claims = generate_admin_claims(); + let claims = match generate_admin_claims() { + Ok(claims) => claims, + Err(e) => return Err(AdminResponse::Ok(render_admin_login(Some(e.message()), redirect.as_deref()))), + }; let jwt = encode_jwt(&claims); let cookie = Cookie::build((COOKIE_NAME, jwt)) .path(admin_path()) - .max_age(time::Duration::minutes(CONFIG.admin_session_lifetime())) + .max_age(time::Duration::minutes(i64::from(CONFIG.admin_session_lifetime()))) .same_site(SameSite::Strict) .http_only(true) .secure(secure.https); diff --git a/src/api/core/events.rs b/src/api/core/events.rs index b7323b5f..0d9f32ad 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -347,8 +347,8 @@ async fn log_event_impl( pub async fn event_cleanup_job(pool: DbPool) { debug!("Start events cleanup job"); - if CONFIG.events_days_retain().is_none() { - debug!("events_days_retain is not configured, skipping"); + if CONFIG.events_days_retain().is_none_or(|d| d == 0) { + debug!("events_days_retain is not configured or set to 0, skipping"); return; } diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index 531eea58..66f2a89e 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -294,7 +294,7 @@ pub async fn validate_email_code_str( if !crypto::ct_eq(issued_token, token) { email_data.add_attempt(); - if email_data.attempts >= CONFIG.email_attempts_limit() { + if email_data.attempts >= u64::from(CONFIG.email_attempts_limit()) { email_data.reset_token(); } twofactor.data = email_data.to_json(); @@ -313,7 +313,7 @@ pub async fn validate_email_code_str( twofactor.save(conn).await?; let dt = DateTime::from_timestamp(email_data.token_sent, 0).expect("Email token timestamp invalid.").naive_utc(); - let max_time = CONFIG.email_expiration_time().cast_signed(); + let max_time = i64::from(CONFIG.email_expiration_time()); if dt + TimeDelta::try_seconds(max_time).unwrap() < Utc::now().naive_utc() { err!( "Token has expired", diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index 5be8b886..db533b50 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -206,7 +206,7 @@ pub async fn send_incomplete_2fa_notifications(pool: DbPool) { }; let now = Utc::now().naive_utc(); - let Some(time_limit) = TimeDelta::try_minutes(CONFIG.incomplete_2fa_time_limit()) else { + let Some(time_limit) = TimeDelta::try_minutes(i64::from(CONFIG.incomplete_2fa_time_limit())) else { error!("Failed to calculate the time limit for sending incomplete 2fa attempts"); return; }; diff --git a/src/api/core/two_factor/protected_actions.rs b/src/api/core/two_factor/protected_actions.rs index 1cafede0..bf757d14 100644 --- a/src/api/core/two_factor/protected_actions.rs +++ b/src/api/core/two_factor/protected_actions.rs @@ -128,23 +128,24 @@ pub async fn validate_protected_action_otp( .await .map_res("Protected action token not found, try sending the code again or restart the process")?; let mut pa_data = ProtectedActionData::from_json(&pa.data)?; - pa_data.add_attempt(); - pa.data = pa_data.to_json(); // Fail after x attempts if the token has been used too many times. // Don't delete it, as we use it to keep track of attempts. - if pa_data.attempts >= CONFIG.email_attempts_limit() { + if pa_data.attempts >= u64::from(CONFIG.email_attempts_limit()) { err!("Token has expired") } // Check if the token has expired (Using the email 2fa expiration time) - let max_time = CONFIG.email_expiration_time().cast_signed(); + let max_time = i64::from(CONFIG.email_expiration_time()); if pa_data.time_since_sent().num_seconds() > max_time { pa.delete(conn).await?; err!("Token has expired") } if !crypto::ct_eq(&pa_data.token, otp) { + pa_data.add_attempt(); + pa.data = pa_data.to_json(); + pa.save(conn).await?; err!("Token is invalid") } diff --git a/src/api/icons.rs b/src/api/icons.rs index 05027959..178f2965 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -62,7 +62,7 @@ static CLIENT: LazyLock = LazyLock::new(|| { // Generate the cookie store let cookie_store = Arc::new(Jar::default()); - let icon_download_timeout = Duration::from_secs(CONFIG.icon_download_timeout()); + let icon_download_timeout = Duration::from_secs(u64::from(CONFIG.icon_download_timeout())); let pool_idle_timeout = Duration::from_secs(10); // Reuse the client between requests get_reqwest_client_builder(true) @@ -193,14 +193,14 @@ async fn get_cached_icon(path: &str) -> Option> { None } -async fn file_is_expired(path: &str, ttl: u64) -> Result { +async fn file_is_expired(path: &str, ttl: u32) -> Result { let operator = CONFIG.opendal_operator_for_path_type(&PathType::IconCache)?; let meta = operator.stat(path).await?; let modified = meta.last_modified().ok_or_else(|| std::io::Error::other(format!("No last modified time for `{path}`")))?; let age = SystemTime::now().duration_since(modified.into())?; - Ok(ttl > 0 && ttl <= age.as_secs()) + Ok(ttl > 0 && u64::from(ttl) <= age.as_secs()) } async fn icon_is_negcached(path: &str) -> bool { diff --git a/src/api/identity.rs b/src/api/identity.rs index b61258cf..a7a18cf7 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -447,9 +447,9 @@ async fn password_login(data: ConnectData, user_id: &mut Option, conn: & if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() { if user.last_verifying_at.is_none() || now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds() - > CONFIG.signups_verify_resend_time().cast_signed() + > i64::from(CONFIG.signups_verify_resend_time()) { - let resend_limit = CONFIG.signups_verify_resend_limit().cast_signed(); + let resend_limit = i32::from(CONFIG.signups_verify_resend_limit()); if resend_limit == 0 || user.login_verify_count < resend_limit { // We want to send another email verification if we require signups to verify // their email address, and we haven't sent them a reminder in a while... diff --git a/src/auth.rs b/src/auth.rs index 57720382..27a156e6 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -314,6 +314,17 @@ impl LoginJwtClaims { } } +fn calculate_invite_expire_hours(now: &DateTime) -> Result, Error> { + let expire_hours = i64::from(CONFIG.invitation_expiration_hours()); + let Some(hours_delta) = TimeDelta::try_hours(expire_hours) else { + err!("Unable to calculate time delta for invite expire hours. Check `INVITATION_EXPIRATION_HOURS`."); + }; + let Some(exp) = now.checked_add_signed(hours_delta) else { + err!("Unable to calculate invitation expire date. Check `INVITATION_EXPIRATION_HOURS`."); + }; + Ok(exp) +} + #[derive(Debug, Serialize, Deserialize)] pub struct InviteJwtClaims { // Not before @@ -337,19 +348,19 @@ pub fn generate_invite_claims( org_id: OrganizationId, member_id: MembershipId, invited_by_email: Option, -) -> InviteJwtClaims { +) -> Result { let time_now = Utc::now(); - let expire_hours = i64::from(CONFIG.invitation_expiration_hours()); - InviteJwtClaims { + let exp = calculate_invite_expire_hours(&time_now)?; + Ok(InviteJwtClaims { nbf: time_now.timestamp(), - exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(), + exp: exp.timestamp(), iss: JWT_INVITE_ISSUER.to_string(), sub: user_id, email, org_id, member_id, invited_by_email, - } + }) } #[derive(Debug, Serialize, Deserialize)] @@ -375,19 +386,19 @@ pub fn generate_emergency_access_invite_claims( emer_id: EmergencyAccessId, grantor_name: String, grantor_email: String, -) -> EmergencyAccessInviteJwtClaims { +) -> Result { let time_now = Utc::now(); - let expire_hours = i64::from(CONFIG.invitation_expiration_hours()); - EmergencyAccessInviteJwtClaims { + let exp = calculate_invite_expire_hours(&time_now)?; + Ok(EmergencyAccessInviteJwtClaims { nbf: time_now.timestamp(), - exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(), + exp: exp.timestamp(), iss: JWT_EMERGENCY_ACCESS_INVITE_ISSUER.to_string(), sub: user_id, email, emer_id, grantor_name, grantor_email, - } + }) } #[derive(Debug, Serialize, Deserialize)] @@ -521,36 +532,43 @@ impl BasicJwtClaims { } } -pub fn generate_delete_claims(uuid: String) -> BasicJwtClaims { +pub fn generate_delete_claims(uuid: String) -> Result { let time_now = Utc::now(); - let expire_hours = i64::from(CONFIG.invitation_expiration_hours()); - BasicJwtClaims { + let exp = calculate_invite_expire_hours(&time_now)?; + Ok(BasicJwtClaims { nbf: time_now.timestamp(), - exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(), + exp: exp.timestamp(), iss: JWT_DELETE_ISSUER.to_string(), sub: uuid, - } + }) } -pub fn generate_verify_email_claims(user_id: &UserId) -> BasicJwtClaims { +pub fn generate_verify_email_claims(user_id: &UserId) -> Result { let time_now = Utc::now(); - let expire_hours = i64::from(CONFIG.invitation_expiration_hours()); - BasicJwtClaims { + let exp = calculate_invite_expire_hours(&time_now)?; + Ok(BasicJwtClaims { nbf: time_now.timestamp(), - exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(), + exp: exp.timestamp(), iss: JWT_VERIFYEMAIL_ISSUER.to_string(), sub: user_id.to_string(), - } + }) } -pub fn generate_admin_claims() -> BasicJwtClaims { +pub fn generate_admin_claims() -> Result { let time_now = Utc::now(); - BasicJwtClaims { + let lifetime_minutes = i64::from(CONFIG.admin_session_lifetime()); + let Some(minutes_delta) = TimeDelta::try_minutes(lifetime_minutes) else { + err!("Unable to calculate time delta for admin session lifetime. Check `ADMIN_SESSION_LIFETIME`."); + }; + let Some(exp) = time_now.checked_add_signed(minutes_delta) else { + err!("Unable to calculate admin session lifetime. Check `ADMIN_SESSION_LIFETIME`."); + }; + Ok(BasicJwtClaims { nbf: time_now.timestamp(), - exp: (time_now + TimeDelta::try_minutes(CONFIG.admin_session_lifetime()).unwrap()).timestamp(), + exp: exp.timestamp(), iss: JWT_ADMIN_ISSUER.to_string(), sub: "admin_panel".to_owned(), - } + }) } pub fn generate_send_claims(send_id: &SendId, file_id: &SendFileId) -> BasicJwtClaims { diff --git a/src/config.rs b/src/config.rs index 6c7b5a8b..b822667d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -261,7 +261,13 @@ macro_rules! make_config { async fn from_file() -> Result { let operator = storage::operator_for_path(&CONFIG_FILE_PARENT_DIR)?; - let config_bytes = operator.read(&CONFIG_FILENAME).await?; + let config_bytes = match operator.read(&CONFIG_FILENAME).await { + Ok(bytes) => bytes, + // No config file is fine, should not return any error + Err(e) if e.kind() == opendal::ErrorKind::NotFound => return Ok(Self::default()), + // Any other error might be an issue and should be passed back to the caller + Err(e) => return Err(e.into()), + }; println!("[INFO] Using saved config from `{}` for configuration.\n", *CONFIG_FILE); serde_json::from_slice(&config_bytes.to_vec()).map_err(Into::into) } @@ -539,7 +545,7 @@ make_config! { jobs { /// Job scheduler poll interval |> How often the job scheduler thread checks for jobs to run. /// Set to 0 to globally disable scheduled jobs. - job_poll_interval_ms: u64, false, def, 30_000; + job_poll_interval_ms: u32, false, def, 30_000; /// Send purge schedule |> Cron schedule of the job that checks for Sends past their deletion date. /// Defaults to hourly. Set blank to disable this job. send_purge_schedule: String, false, def, "0 5 * * * *".to_owned(); @@ -595,19 +601,19 @@ make_config! { /// Per-organization attachment storage limit (KB) |> Max kilobytes of attachment storage allowed per org. When this limit is reached, org members will not be allowed to upload further attachments for ciphers owned by that org. org_attachment_limit: i64, true, option; /// Per-user send storage limit (KB) |> Max kilobytes of sends storage allowed per user. When this limit is reached, the user will not be allowed to upload further sends. - user_send_limit: i64, true, option; + user_send_limit: i64, true, option; /// Trash auto-delete days |> Number of days to wait before auto-deleting a trashed item. - /// If unset, trashed items are not auto-deleted. This setting applies globally, so make + /// If unset or 0, trashed items are not auto-deleted. This setting applies globally, so make /// sure to inform all users of any changes to this setting. - trash_auto_delete_days: i64, true, option; + trash_auto_delete_days: u16, true, option; /// Incomplete 2FA time limit |> Number of minutes to wait before a 2FA-enabled login is /// considered incomplete, resulting in an email notification. An incomplete 2FA login is one /// where the correct master password was provided but the required 2FA step was not completed, /// which potentially indicates a master password compromise. Set to 0 to disable this check. /// This setting applies globally to all users. - incomplete_2fa_time_limit: i64, true, def, 3; + incomplete_2fa_time_limit: u16, true, def, 3; /// Disable icon downloads |> Set to true to disable icon downloading in the internal icon service. /// This still serves existing icons from $ICON_CACHE_FOLDER, without generating any external @@ -620,9 +626,9 @@ make_config! { /// this will prevent logins from succeeding until the address has been verified signups_verify: bool, true, def, false; /// If signups require email verification, automatically re-send verification email if it hasn't been sent for a while (in seconds) - signups_verify_resend_time: u64, true, def, 3_600; + signups_verify_resend_time: u32, true, def, 3_600; /// If signups require email verification, limit how many emails are automatically sent when login is attempted (0 means no limit) - signups_verify_resend_limit: u32, true, def, 6; + signups_verify_resend_limit: u16, true, def, 6; /// Email domain whitelist |> Allow signups only from this list of comma-separated domains, even when signups are otherwise disabled signups_domains_whitelist: String, true, def, String::new(); /// Enable event logging |> Enables event logging for organizations. @@ -634,7 +640,7 @@ make_config! { invitations_allowed: bool, true, def, true; /// Invitation token expiration time (in hours) |> The number of hours after which an organization invite token, emergency access invite token, /// email verification token and deletion request token will expire (must be at least 1) - invitation_expiration_hours: u32, false, def, 120; + invitation_expiration_hours: u16, false, def, 120; /// Enable emergency access |> Controls whether users can enable emergency access to their accounts. This setting applies globally to all users. emergency_access_allowed: bool, true, def, true; /// Allow email change |> Controls whether users can change their email. This setting applies globally to all users. @@ -655,8 +661,8 @@ make_config! { /// Invitation organization name |> Name shown in the invitation emails that don't come from a specific organization invitation_org_name: String, true, def, "Vaultwarden".to_owned(); - /// Events days retain |> Number of days to retain events stored in the database. If unset, events are kept indefinitely. - events_days_retain: i64, false, option; + /// Events days retain |> Number of days to retain events stored in the database. If unset or 0, events are kept indefinitely. + events_days_retain: u16, false, option; }, client { @@ -694,13 +700,13 @@ make_config! { /// Temporary redirects are useful while testing different icon services, but once a service /// has been decided on, consider using permanent redirects for cacheability. The legacy codes /// are currently better supported by the Bitwarden clients. - icon_redirect_code: u32, true, def, 302; + icon_redirect_code: u16, true, def, 302; /// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be refreshed - icon_cache_ttl: u64, true, def, 2_592_000; + icon_cache_ttl: u32, true, def, 2_592_000; /// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again. - icon_cache_negttl: u64, true, def, 259_200; + icon_cache_negttl: u32, true, def, 259_200; /// Icon download timeout |> Number of seconds when to stop attempting to download an icon. - icon_download_timeout: u64, true, def, 10; + icon_download_timeout: u16, true, def, 10; /// [Deprecated] Icon blacklist Regex |> Use `http_request_block_regex` instead icon_blacklist_regex: String, false, option; @@ -751,11 +757,11 @@ make_config! { /// Max database connection retries |> Number of times to retry the database connection during startup, with 1 second between each retry, set to 0 to retry indefinitely db_connection_retries: u32, false, def, 15; - /// Timeout when acquiring database connection - database_timeout: u64, false, def, 30; + /// Timeout in seconds when acquiring database connection + database_timeout: u32, false, def, 30; /// Timeout in seconds before idle connections to the database are closed - database_idle_timeout: u64, false, def, 600; + database_idle_timeout: u32, false, def, 600; /// Database connection max pool size database_max_conns: u32, false, def, 10; @@ -776,22 +782,22 @@ make_config! { allowed_connect_src: String, true, def, String::new(); /// Seconds between login requests |> Number of seconds, on average, between login and 2FA requests from the same IP address before rate limiting kicks in - login_ratelimit_seconds: u64, false, def, 60; + login_ratelimit_seconds: u16, false, def, 60; /// Max burst size for login requests |> Allow a burst of requests of up to this size, while maintaining the average indicated by `login_ratelimit_seconds`. Note that this applies to both the login and the 2FA, so it's recommended to allow a burst size of at least 2 - login_ratelimit_max_burst: u32, false, def, 10; + login_ratelimit_max_burst: u16, false, def, 10; /// Seconds between unauthenticated requests |> Number of seconds, on average, between requests from the same IP address to any of the rate limited unauthenticated endpoints - unauthenticated_ratelimit_seconds: u64, false, def, 60; + unauthenticated_ratelimit_seconds: u16, false, def, 60; /// Max burst size for unauthenticated requests |> Allow a burst of requests of up to this size, while maintaining the average indicated by `unauthenticated_ratelimit_seconds`. This is shared between several endpoints, so it needs to be more lenient than the login one - unauthenticated_ratelimit_max_burst: u32, false, def, 50; + unauthenticated_ratelimit_max_burst: u16, false, def, 50; /// Seconds between admin login requests |> Number of seconds, on average, between admin requests from the same IP address before rate limiting kicks in - admin_ratelimit_seconds: u64, false, def, 300; + admin_ratelimit_seconds: u16, false, def, 300; /// Max burst size for admin login requests |> Allow a burst of requests of up to this size, while maintaining the average indicated by `admin_ratelimit_seconds` - admin_ratelimit_max_burst: u32, false, def, 3; + admin_ratelimit_max_burst: u16, false, def, 3; /// Admin session lifetime |> Set the lifetime of admin sessions to this value (in minutes). - admin_session_lifetime: i64, true, def, 20; + admin_session_lifetime: u32, true, def, 20; /// Enable groups (BETA!) (Know the risks!) |> Enables groups support for organizations (Currently contains known issues!). org_groups_enabled: bool, false, def, false; @@ -844,8 +850,8 @@ make_config! { sso_master_password_policy: String, true, option; /// Use SSO only for auth not the session lifecycle |> Use default Vaultwarden session lifecycle (Idle refresh token valid for 30days) sso_auth_only_not_session: bool, true, def, false; - /// Client cache for discovery endpoint. |> Duration in seconds (0 or less to disable). More details: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect#client-cache - sso_client_cache_expiration: u64, true, def, 0; + /// Client cache for discovery endpoint. |> Duration in seconds (0 to disable). More details: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect#client-cache + sso_client_cache_expiration: u32, true, def, 0; /// Log all tokens |> `LOG_LEVEL=debug` or `LOG_LEVEL=info,vaultwarden::sso=debug` is required sso_debug_tokens: bool, true, def, false; }, @@ -903,11 +909,11 @@ make_config! { /// SMTP Auth mechanism |> Defaults for SSL is "Plain" and "Login" and nothing for Non-SSL connections. Possible values: ["Plain", "Login", "Xoauth2"]. Multiple options need to be separated by a comma ','. smtp_auth_mechanism: String, true, option; /// SMTP connection timeout |> Number of seconds when to stop trying to connect to the SMTP server - smtp_timeout: u64, true, def, 15; + smtp_timeout: u16, true, def, 15; /// Server name sent during HELO |> By default this value should be the machine's hostname, but might need to be changed in case it trips some anti-spam filters helo_name: String, true, option; /// Embed images as email attachments. - smtp_embed_images: bool, true, def, true; + smtp_embed_images: bool, true, def, true; /// _smtp_img_src _smtp_img_src: String, false, generated, |c| generate_smtp_img_src(c.smtp_embed_images, &c.domain); /// Enable SMTP debugging (Know the risks!) |> DANGEROUS: Enabling this will output very detailed SMTP messages. This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting! @@ -925,9 +931,9 @@ make_config! { /// Email token size |> Number of digits in an email 2FA token (min: 6, max: 255). Note that the Bitwarden clients are hardcoded to mention 6 digit codes regardless of this setting. email_token_size: u8, true, def, 6; /// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token. - email_expiration_time: u64, true, def, 600; + email_expiration_time: u32, true, def, 600; /// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent - email_attempts_limit: u64, true, def, 3; + email_attempts_limit: u8, true, def, 3; /// Setup email 2FA at signup |> Setup email 2FA provider on registration regardless of any organization policy email_2fa_enforce_on_verified_invite: bool, true, def, false; /// Auto-enable 2FA (Know the risks!) |> Automatically setup email 2FA as fallback provider when needed @@ -983,7 +989,15 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { } if cfg.database_min_conns > cfg.database_max_conns { - err!("`DATABASE_MIN_CONNS` must be smaller than or equal to `DATABASE_MAX_CONNS`."); + err!("`DATABASE_MIN_CONNS` must be less than or equal to `DATABASE_MAX_CONNS`."); + } + + if cfg.database_timeout < 1 { + err!("`DATABASE_TIMEOUT` must be at least 1 second"); + } + + if cfg.database_idle_timeout < 1 { + err!("`DATABASE_IDLE_TIMEOUT` must be at least 1 second"); } if let Some(log_file) = &cfg.log_file @@ -1274,22 +1288,28 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { err!("`PURGE_INCOMPLETE_SSO_AUTH` is not a valid cron expression") } - if cfg.trash_auto_delete_days.is_some_and(|d| d < 0) { - err!( - "`TRASH_AUTO_DELETE_DAYS` is less than 0 days, negative values are not allowed, unset to disable this feature" - ); + if cfg.login_ratelimit_seconds < 1 { + err!("`LOGIN_RATELIMIT_SECONDS` must be at least 1 second"); } - if cfg.incomplete_2fa_time_limit < 0 { - err!( - "`INCOMPLETE_2FA_TIME_LIMIT` is less than 0 minutes, negative values are not allowed, use 0 to disable this feature" - ); + if cfg.login_ratelimit_max_burst < 1 { + err!("`LOGIN_RATELIMIT_MAX_BURST` must be at least 1"); } - if cfg.events_days_retain.is_some_and(|d| d < 0) { - err!( - "`EVENTS_DAYS_RETAIN` is less than 0 days, negative values are not allowed, unset to disable this feature" - ); + if cfg.admin_ratelimit_seconds < 1 { + err!("`ADMIN_RATELIMIT_SECONDS` must be at least 1 second"); + } + + if cfg.admin_ratelimit_max_burst < 1 { + err!("`ADMIN_RATELIMIT_MAX_BURST` must be at least 1"); + } + + if cfg.unauthenticated_ratelimit_seconds < 1 { + err!("`UNAUTHENTICATED_RATELIMIT_SECONDS` must be at least 1 second"); + } + + if cfg.unauthenticated_ratelimit_max_burst < 1 { + err!("`UNAUTHENTICATED_RATELIMIT_MAX_BURST` must be at least 1"); } if !cfg.disable_admin_token { @@ -1465,7 +1485,19 @@ impl Config { pub async fn load() -> Result { // Loading from env and file let env = ConfigBuilder::from_env(); - let usr = ConfigBuilder::from_file().await.unwrap_or_default(); + let usr = ConfigBuilder::from_file().await.unwrap_or_else(|e| { + println!( + "[ERROR] Failed to load configuration from `{}`\n\ + #####################################################################################\n\ + # The file seems to exist but might contain invalid JSON or invalid values. #\n\ + # Check the syntax and make sure every value is valid and within its allowed range. #\n\ + # This file is not used until those errors are resolved. #\n\ + #####################################################################################\n\ + {e:?}\n", + *CONFIG_FILE + ); + ConfigBuilder::default() + }); // Create merged config, config file overwrites env let mut overrides = Vec::new(); diff --git a/src/db/mod.rs b/src/db/mod.rs index 2eae3f3c..2ccd7d48 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -212,8 +212,8 @@ impl DbPool { let pool = Pool::builder() .max_size(max_conns) .min_idle(Some(CONFIG.database_min_conns())) - .idle_timeout(Some(Duration::from_secs(CONFIG.database_idle_timeout()))) - .connection_timeout(Duration::from_secs(CONFIG.database_timeout())) + .idle_timeout(Some(Duration::from_secs(u64::from(CONFIG.database_idle_timeout())))) + .connection_timeout(Duration::from_secs(u64::from(CONFIG.database_timeout()))) .connection_customizer(Box::new(DbConnOptions { init_stmts: conn_type.get_init_stmts(), })) @@ -233,7 +233,7 @@ impl DbPool { // Get a connection from the pool pub async fn get(&self) -> Result { - let duration = Duration::from_secs(CONFIG.database_timeout()); + let duration = Duration::from_secs(u64::from(CONFIG.database_timeout())); let permit = match timeout(duration, Arc::clone(&self.semaphore).acquire_owned()).await { Ok(p) => p.expect("Semaphore should be open"), Err(_) => { diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 9792275b..fca03653 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -494,19 +494,22 @@ impl Cipher { /// Purge all ciphers that are old enough to be auto-deleted. pub async fn purge_trash(conn: &DbConn) { - if let Some(auto_delete_days) = CONFIG.trash_auto_delete_days() { - let Some(days) = TimeDelta::try_days(auto_delete_days) else { - error!("Unable to calculate auto delete days!"); - return; - }; - let now = Utc::now().naive_utc(); - let Some(dt) = now.checked_sub_signed(days) else { - error!("Unable to calculate trash purge date!"); - return; - }; - for cipher in Self::find_deleted_before(&dt, conn).await { - cipher.delete(conn).await.ok(); - } + let Some(auto_delete_days) = CONFIG.trash_auto_delete_days().filter(|&d| d != 0) else { + debug!("trash_auto_delete_days is not configured or set to 0, skipping"); + return; + }; + + let Some(days) = TimeDelta::try_days(i64::from(auto_delete_days)) else { + error!("Unable to calculate auto delete days!"); + return; + }; + let now = Utc::now().naive_utc(); + let Some(dt) = now.checked_sub_signed(days) else { + error!("Unable to calculate trash purge date!"); + return; + }; + for cipher in Self::find_deleted_before(&dt, conn).await { + cipher.delete(conn).await.ok(); } } diff --git a/src/db/models/event.rs b/src/db/models/event.rs index f48f2526..8dc3a5ad 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -347,11 +347,11 @@ impl Event { } pub async fn clean_events(conn: &DbConn) -> EmptyResult { - let Some(days_to_retain) = CONFIG.events_days_retain() else { + let Some(days_to_retain) = CONFIG.events_days_retain().filter(|&d| d != 0) else { return Ok(()); }; - let Some(days_to_retain) = TimeDelta::try_days(days_to_retain) else { + let Some(days_to_retain) = TimeDelta::try_days(i64::from(days_to_retain)) else { err!("Unable to calculate the event retention date, check 'EVENTS_DAYS_RETAIN'") }; diff --git a/src/mail.rs b/src/mail.rs index 2efb8d70..6ebbc806 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -36,7 +36,7 @@ fn smtp_transport() -> AsyncSmtpTransport { let smtp_client = AsyncSmtpTransport::::builder_dangerous(host.as_str()) .port(CONFIG.smtp_port()) - .timeout(Some(Duration::from_secs(CONFIG.smtp_timeout()))); + .timeout(Some(Duration::from_secs(u64::from(CONFIG.smtp_timeout())))); // Determine security let smtp_client = if CONFIG.smtp_security() == *"off" { @@ -169,7 +169,7 @@ pub async fn send_password_hint(address: &str, hint: Option) -> EmptyRes } pub async fn send_delete_account(address: &str, user_id: &UserId) -> EmptyResult { - let claims = generate_delete_claims(user_id.to_string()); + let claims = generate_delete_claims(user_id.to_string())?; let delete_token = encode_jwt(&claims); let (subject, body_html, body_text) = get_text( @@ -187,7 +187,7 @@ pub async fn send_delete_account(address: &str, user_id: &UserId) -> EmptyResult } pub async fn send_verify_email(address: &str, user_id: &UserId) -> EmptyResult { - let claims = generate_verify_email_claims(user_id); + let claims = generate_verify_email_claims(user_id)?; let verify_email_token = encode_jwt(&claims); let (subject, body_html, body_text) = get_text( @@ -237,7 +237,7 @@ pub async fn send_welcome(address: &str) -> EmptyResult { } pub async fn send_welcome_must_verify(address: &str, user_id: &UserId) -> EmptyResult { - let claims = generate_verify_email_claims(user_id); + let claims = generate_verify_email_claims(user_id)?; let verify_email_token = encode_jwt(&claims); let (subject, body_html, body_text) = get_text( @@ -292,7 +292,7 @@ pub async fn send_invite( org_id.clone(), member_id.clone(), invited_by_email, - ); + )?; let invite_token = encode_jwt(&claims); let mut query = url::Url::parse("https://query.builder").unwrap(); { @@ -351,7 +351,7 @@ pub async fn send_emergency_access_invite( emer_id.clone(), String::from(grantor_name), String::from(grantor_email), - ); + )?; // Build the query here to ensure proper escaping let mut query = url::Url::parse("https://query.builder").unwrap(); diff --git a/src/main.rs b/src/main.rs index f27729d6..6a7f18f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -729,7 +729,7 @@ fn schedule_jobs(pool: db::DbPool) { // Cleanup the event table of records x days old. if CONFIG.org_events_enabled() && !CONFIG.event_cleanup_schedule().is_empty() - && CONFIG.events_days_retain().is_some() + && CONFIG.events_days_retain().is_some_and(|d| d != 0) { sched.add(Job::new(CONFIG.event_cleanup_schedule().parse().unwrap(), || { runtime.spawn(api::event_cleanup_job(pool.clone())); @@ -754,7 +754,9 @@ fn schedule_jobs(pool: db::DbPool) { // tick, the one that was added earlier will run first. loop { sched.tick(); - runtime.block_on(tokio::time::sleep(tokio::time::Duration::from_millis(CONFIG.job_poll_interval_ms()))); + runtime.block_on(tokio::time::sleep(tokio::time::Duration::from_millis(u64::from( + CONFIG.job_poll_interval_ms(), + )))); } }) .expect("Error spawning job scheduler thread"); diff --git a/src/ratelimit.rs b/src/ratelimit.rs index 70217957..50d1f99b 100644 --- a/src/ratelimit.rs +++ b/src/ratelimit.rs @@ -7,20 +7,20 @@ use crate::{CONFIG, Error}; type Limiter = RateLimiter, DefaultClock>; static LIMITER_LOGIN: LazyLock = LazyLock::new(|| { - let seconds = Duration::from_secs(CONFIG.login_ratelimit_seconds()); - let burst = NonZeroU32::new(CONFIG.login_ratelimit_max_burst()).expect("Non-zero login ratelimit burst"); + let seconds = Duration::from_secs(u64::from(CONFIG.login_ratelimit_seconds())); + let burst = NonZeroU32::new(u32::from(CONFIG.login_ratelimit_max_burst())).expect("Non-zero login ratelimit burst"); RateLimiter::keyed(Quota::with_period(seconds).expect("Non-zero login ratelimit seconds").allow_burst(burst)) }); static LIMITER_ADMIN: LazyLock = LazyLock::new(|| { - let seconds = Duration::from_secs(CONFIG.admin_ratelimit_seconds()); - let burst = NonZeroU32::new(CONFIG.admin_ratelimit_max_burst()).expect("Non-zero admin ratelimit burst"); + let seconds = Duration::from_secs(u64::from(CONFIG.admin_ratelimit_seconds())); + let burst = NonZeroU32::new(u32::from(CONFIG.admin_ratelimit_max_burst())).expect("Non-zero admin ratelimit burst"); RateLimiter::keyed(Quota::with_period(seconds).expect("Non-zero admin ratelimit seconds").allow_burst(burst)) }); static LIMITER_UNAUTHENTICATED: LazyLock = LazyLock::new(|| { - let seconds = Duration::from_secs(CONFIG.unauthenticated_ratelimit_seconds()); - let burst = NonZeroU32::new(CONFIG.unauthenticated_ratelimit_max_burst()) + let seconds = Duration::from_secs(u64::from(CONFIG.unauthenticated_ratelimit_seconds())); + let burst = NonZeroU32::new(u32::from(CONFIG.unauthenticated_ratelimit_max_burst())) .expect("Non-zero unauthenticated ratelimit burst"); RateLimiter::keyed( Quota::with_period(seconds).expect("Non-zero unauthenticated ratelimit seconds").allow_burst(burst), diff --git a/src/sso_client.rs b/src/sso_client.rs index a5dcaa83..a66d75f6 100644 --- a/src/sso_client.rs +++ b/src/sso_client.rs @@ -29,7 +29,7 @@ static CLIENT_CACHE_KEY: LazyLock = LazyLock::new(|| "sso-client".to_own static CLIENT_CACHE: LazyLock> = LazyLock::new(|| { moka::sync::Cache::builder() .max_capacity(1) - .time_to_live(Duration::from_secs(CONFIG.sso_client_cache_expiration())) + .time_to_live(Duration::from_secs(u64::from(CONFIG.sso_client_cache_expiration()))) .build() }); static REFRESH_CACHE: LazyLock>> = diff --git a/src/util.rs b/src/util.rs index 9d319542..ea4c78ef 100644 --- a/src/util.rs +++ b/src/util.rs @@ -210,7 +210,7 @@ impl Fairing for Cors { pub struct Cached { response: R, is_immutable: bool, - ttl: u64, + ttl: u32, } impl Cached { @@ -230,7 +230,7 @@ impl Cached { } } - pub fn ttl(response: R, ttl: u64, is_immutable: bool) -> Cached { + pub fn ttl(response: R, ttl: u32, is_immutable: bool) -> Cached { Self { response, is_immutable, @@ -251,7 +251,7 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for Cache res.set_raw_header("Cache-Control", cache_control_header); let time_now = Local::now(); - let expiry_time = time_now + chrono::TimeDelta::try_seconds(self.ttl.try_into().unwrap()).unwrap(); + let expiry_time = time_now + chrono::TimeDelta::try_seconds(self.ttl.into()).unwrap(); res.set_raw_header("Expires", format_datetime_http(&expiry_time)); Ok(res) } @@ -451,7 +451,13 @@ pub fn get_env(key: &str) -> Option where V: FromStr, { - try_parse_string(get_env_str_value(key)) + let value = get_env_str_value(key); + let value_is_some = value.is_some(); + let parsed: Option = try_parse_string(value); + if parsed.is_none() && value_is_some { + println!("[ERROR] `{key}` contains an invalid value for its type and is ignored, the default is used instead."); + } + parsed } pub fn get_env_bool(key: &str) -> Option { @@ -461,6 +467,10 @@ pub fn get_env_bool(key: &str) -> Option { match get_env_str_value(key) { Some(val) if TRUE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(true), Some(val) if FALSE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(false), + Some(_) => { + println!("[ERROR] `{key}` contains an invalid boolean value and is ignored, the default is used instead."); + None + } _ => None, } }