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.
This commit is contained in:
parent
1f2ec0fc14
commit
830d424297
2 changed files with 27 additions and 1 deletions
|
|
@ -96,6 +96,7 @@ pub(crate) async fn update_roaster(
|
||||||
Path(id): Path<RoasterId>,
|
Path(id): Path<RoasterId>,
|
||||||
Json(payload): Json<UpdateRoaster>,
|
Json(payload): Json<UpdateRoaster>,
|
||||||
) -> Result<Json<Roaster>, ApiError> {
|
) -> Result<Json<Roaster>, ApiError> {
|
||||||
|
let payload = payload.normalize();
|
||||||
let has_changes = payload.name.is_some()
|
let has_changes = payload.name.is_some()
|
||||||
|| payload.country.is_some()
|
|| payload.country.is_some()
|
||||||
|| payload.city.is_some()
|
|| payload.city.is_some()
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ impl NewRoaster {
|
||||||
self.name = self.name.trim().to_string();
|
self.name = self.name.trim().to_string();
|
||||||
self.country = self.country.trim().to_string();
|
self.country = self.country.trim().to_string();
|
||||||
self.city = normalize_optional_field(self.city);
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,6 +54,13 @@ fn normalize_optional_field(value: Option<String>) -> Option<String> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
impl Roaster {
|
||||||
pub fn to_timeline_event(&self) -> NewTimelineEvent {
|
pub fn to_timeline_event(&self) -> NewTimelineEvent {
|
||||||
let mut details = vec![TimelineEventDetail {
|
let mut details = vec![TimelineEventDetail {
|
||||||
|
|
@ -88,6 +96,23 @@ pub struct UpdateRoaster {
|
||||||
pub homepage: Option<String>,
|
pub homepage: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
pub enum RoasterSortKey {
|
pub enum RoasterSortKey {
|
||||||
CreatedAt,
|
CreatedAt,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue