From d37a0a4219f0bfe6f83070bb006e1d20de6ff326 Mon Sep 17 00:00:00 2001 From: easonysliu Date: Thu, 9 Apr 2026 14:30:36 +0800 Subject: [PATCH 1/2] fix: return error instead of panicking on invalid cipher type in to_json `Cipher::to_json()` returns `Result` but its match arm for unknown `atype` values called `panic!("Wrong type")` instead of propagating an error. This means if a cipher with an invalid/unknown type ends up in the database (via direct DB edits, data migration issues, or future type additions in the upstream Bitwarden protocol), the entire server process would crash on the next sync request. Replace the `panic!` with `err!()` so callers receive a proper `Err` and can handle or log it gracefully without taking down the server. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/db/models/cipher.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index b28a25cd..ac2d9f4a 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -398,7 +398,7 @@ impl Cipher { 3 => "card", 4 => "identity", 5 => "sshKey", - _ => panic!("Wrong type"), + _ => err!(format!("Cipher {} has an invalid type {}", self.uuid, self.atype)), }; json_object[key] = type_data_json; From eb3205a1969fe4afbdce1f49313de3df3045b078 Mon Sep 17 00:00:00 2001 From: easonysliu Date: Thu, 9 Apr 2026 14:35:51 +0800 Subject: [PATCH 2/2] fix: parse_date() panics on invalid date input from HTTP query params The parse_date() utility function called .unwrap() directly on user- controlled date strings from HTTP query parameters, allowing any authenticated user to crash the server with a malformed RFC3339 date. Change parse_date() to return Result and update all callers in events.rs to propagate the error with ?, returning a 400 Bad Request instead of panicking. The one call-site using a hardcoded literal (sends.rs) uses .expect() with an explanatory message. Affected endpoints: - GET /organizations/{id}/events?start=...&end=... - GET /ciphers/{id}/events?start=...&end=... - GET /organizations/{id}/users/{id}/events?start=...&end=... - POST /collect (event collection endpoint) --- src/api/core/events.rs | 20 ++++++++++---------- src/api/core/sends.rs | 2 +- src/util.rs | 7 +++++-- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/api/core/events.rs b/src/api/core/events.rs index 2f33a407..d74344f7 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -41,11 +41,11 @@ async fn get_org_events(org_id: OrganizationId, data: EventRange, headers: Admin let events_json: Vec = if !CONFIG.org_events_enabled() { Vec::with_capacity(0) } else { - let start_date = parse_date(&data.start); + let start_date = parse_date(&data.start)?; let end_date = if let Some(before_date) = &data.continuation_token { - parse_date(before_date) + parse_date(before_date)? } else { - parse_date(&data.end) + parse_date(&data.end)? }; Event::find_by_organization_uuid(&org_id, &start_date, &end_date, &conn) @@ -71,11 +71,11 @@ async fn get_cipher_events(cipher_id: CipherId, data: EventRange, headers: Heade } else { let mut events_json = Vec::with_capacity(0); if Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &conn).await { - let start_date = parse_date(&data.start); + let start_date = parse_date(&data.start)?; let end_date = if let Some(before_date) = &data.continuation_token { - parse_date(before_date) + parse_date(before_date)? } else { - parse_date(&data.end) + parse_date(&data.end)? }; events_json = Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &conn) @@ -110,11 +110,11 @@ async fn get_user_events( let events_json: Vec = if !CONFIG.org_events_enabled() { Vec::with_capacity(0) } else { - let start_date = parse_date(&data.start); + let start_date = parse_date(&data.start)?; let end_date = if let Some(before_date) = &data.continuation_token { - parse_date(before_date) + parse_date(before_date)? } else { - parse_date(&data.end) + parse_date(&data.end)? }; Event::find_by_org_and_member(&org_id, &member_id, &start_date, &end_date, &conn) @@ -173,7 +173,7 @@ async fn post_events_collect(data: Json>, headers: Headers, } for event in data.iter() { - let event_date = parse_date(&event.date); + let event_date = parse_date(&event.date)?; match event.r#type { 1000..=1099 => { _log_user_event( diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index 10bf85be..6a1982dc 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -23,7 +23,7 @@ use crate::{ const SEND_INACCESSIBLE_MSG: &str = "Send does not exist or is no longer available"; static ANON_PUSH_DEVICE: LazyLock = LazyLock::new(|| { - let dt = crate::util::parse_date("1970-01-01T00:00:00.000000Z"); + let dt = crate::util::parse_date("1970-01-01T00:00:00.000000Z").expect("hardcoded epoch date is always valid"); Device { uuid: String::from("00000000-0000-0000-0000-000000000000").into(), created_at: dt, diff --git a/src/util.rs b/src/util.rs index 6da1c3df..e25c550e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -483,8 +483,11 @@ pub fn format_datetime_http(dt: &DateTime) -> String { expiry_time.to_rfc2822().replace("+0000", "GMT") } -pub fn parse_date(date: &str) -> NaiveDateTime { - DateTime::parse_from_rfc3339(date).unwrap().naive_utc() +pub fn parse_date(date: &str) -> Result { + match DateTime::parse_from_rfc3339(date) { + Ok(dt) => Ok(dt.naive_utc()), + Err(e) => err!(format!("Invalid RFC3339 date '{date}': {e}")), + } } /// Returns true or false if an email address is valid or not