Browse Source

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<NaiveDateTime, Error> 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)
pull/7070/head
easonysliu 2 weeks ago
parent
commit
eb3205a196
  1. 20
      src/api/core/events.rs
  2. 2
      src/api/core/sends.rs
  3. 7
      src/util.rs

20
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<Value> = if !CONFIG.org_events_enabled() { let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
Vec::with_capacity(0) Vec::with_capacity(0)
} else { } 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 { let end_date = if let Some(before_date) = &data.continuation_token {
parse_date(before_date) parse_date(before_date)?
} else { } else {
parse_date(&data.end) parse_date(&data.end)?
}; };
Event::find_by_organization_uuid(&org_id, &start_date, &end_date, &conn) 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 { } else {
let mut events_json = Vec::with_capacity(0); let mut events_json = Vec::with_capacity(0);
if Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &conn).await { 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 { let end_date = if let Some(before_date) = &data.continuation_token {
parse_date(before_date) parse_date(before_date)?
} else { } else {
parse_date(&data.end) parse_date(&data.end)?
}; };
events_json = Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &conn) 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<Value> = if !CONFIG.org_events_enabled() { let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
Vec::with_capacity(0) Vec::with_capacity(0)
} else { } 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 { let end_date = if let Some(before_date) = &data.continuation_token {
parse_date(before_date) parse_date(before_date)?
} else { } else {
parse_date(&data.end) parse_date(&data.end)?
}; };
Event::find_by_org_and_member(&org_id, &member_id, &start_date, &end_date, &conn) 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<Vec<EventCollection>>, headers: Headers,
} }
for event in data.iter() { for event in data.iter() {
let event_date = parse_date(&event.date); let event_date = parse_date(&event.date)?;
match event.r#type { match event.r#type {
1000..=1099 => { 1000..=1099 => {
_log_user_event( _log_user_event(

2
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"; const SEND_INACCESSIBLE_MSG: &str = "Send does not exist or is no longer available";
static ANON_PUSH_DEVICE: LazyLock<Device> = LazyLock::new(|| { static ANON_PUSH_DEVICE: LazyLock<Device> = 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 { Device {
uuid: String::from("00000000-0000-0000-0000-000000000000").into(), uuid: String::from("00000000-0000-0000-0000-000000000000").into(),
created_at: dt, created_at: dt,

7
src/util.rs

@ -483,8 +483,11 @@ pub fn format_datetime_http(dt: &DateTime<Local>) -> String {
expiry_time.to_rfc2822().replace("+0000", "GMT") expiry_time.to_rfc2822().replace("+0000", "GMT")
} }
pub fn parse_date(date: &str) -> NaiveDateTime { pub fn parse_date(date: &str) -> Result<NaiveDateTime, crate::Error> {
DateTime::parse_from_rfc3339(date).unwrap().naive_utc() 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 /// Returns true or false if an email address is valid or not

Loading…
Cancel
Save