diff --git a/src/application/routes/api/roasters.rs b/src/application/routes/api/roasters.rs index 328e383..62c4a88 100644 --- a/src/application/routes/api/roasters.rs +++ b/src/application/routes/api/roasters.rs @@ -96,6 +96,7 @@ pub(crate) async fn update_roaster( Path(id): Path, Json(payload): Json, ) -> Result, ApiError> { + let payload = payload.normalize(); let has_changes = payload.name.is_some() || payload.country.is_some() || payload.city.is_some() diff --git a/src/domain/roasters.rs b/src/domain/roasters.rs index fcd6edf..289999b 100644 --- a/src/domain/roasters.rs +++ b/src/domain/roasters.rs @@ -29,7 +29,8 @@ impl NewRoaster { self.name = self.name.trim().to_string(); self.country = self.country.trim().to_string(); self.city = normalize_optional_field(self.city); - self.homepage = normalize_optional_field(self.homepage); + self.homepage = + normalize_optional_field(self.homepage).filter(|url| is_valid_url_scheme(url)); self } @@ -53,6 +54,13 @@ fn normalize_optional_field(value: Option) -> Option { }) } +/// Returns `true` if the URL starts with `http://` or `https://`. +/// Rejects `javascript:`, `data:`, and other potentially dangerous schemes. +fn is_valid_url_scheme(url: &str) -> bool { + let lower = url.trim().to_lowercase(); + lower.starts_with("http://") || lower.starts_with("https://") +} + impl Roaster { pub fn to_timeline_event(&self) -> NewTimelineEvent { let mut details = vec![TimelineEventDetail { @@ -88,6 +96,23 @@ pub struct UpdateRoaster { pub homepage: Option, } +impl UpdateRoaster { + pub fn normalize(mut self) -> Self { + self.homepage = self + .homepage + .and_then(|h| { + let trimmed = h.trim().to_string(); + if trimmed.is_empty() { + None + } else { + Some(trimmed) + } + }) + .filter(|url| is_valid_url_scheme(url)); + self + } +} + #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum RoasterSortKey { CreatedAt,