From 830d424297344f91699633cb77e92479bb8794c1 Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Fri, 6 Feb 2026 17:52:15 +0000 Subject: [PATCH] fix(security): add server-side URL scheme validation for roaster homepage Reject non-http(s) URL schemes (javascript:, data:, etc.) in both NewRoaster::normalize() and new UpdateRoaster::normalize(). The HTML input type="url" provides browser-side validation, but the API path had no server-side check. --- src/application/routes/api/roasters.rs | 1 + src/domain/roasters.rs | 27 +++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) 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,