pinakes-server: validate GPS coordinate bounds; validate saved search fields and sort_order

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Idca86117aeeff4afd489ee00bb5c70a36a6a6964
This commit is contained in:
raf 2026-03-12 20:47:44 +03:00
commit 61eb2335d3
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
4 changed files with 51 additions and 0 deletions

View file

@ -152,6 +152,14 @@ pub async fn get_map_photos(
State(state): State<AppState>, State(state): State<AppState>,
Query(query): Query<MapQuery>, Query(query): Query<MapQuery>,
) -> Result<impl IntoResponse, ApiError> { ) -> Result<impl IntoResponse, ApiError> {
let valid_lat = |v: f64| v.is_finite() && (-90.0..=90.0).contains(&v);
let valid_lon = |v: f64| v.is_finite() && (-180.0..=180.0).contains(&v);
if !valid_lat(query.lat1) || !valid_lat(query.lat2) {
return Err(ApiError::bad_request("latitude must be in [-90, 90]"));
}
if !valid_lon(query.lon1) || !valid_lon(query.lon2) {
return Err(ApiError::bad_request("longitude must be in [-180, 180]"));
}
// Validate bounding box // Validate bounding box
let min_lat = query.lat1.min(query.lat2); let min_lat = query.lat1.min(query.lat2);
let max_lat = query.lat1.max(query.lat2); let max_lat = query.lat1.max(query.lat2);

View file

@ -22,10 +22,43 @@ pub struct SavedSearchResponse {
pub created_at: chrono::DateTime<chrono::Utc>, pub created_at: chrono::DateTime<chrono::Utc>,
} }
const VALID_SORT_ORDERS: &[&str] = &[
"date_asc",
"date_desc",
"name_asc",
"name_desc",
"size_asc",
"size_desc",
];
pub async fn create_saved_search( pub async fn create_saved_search(
State(state): State<AppState>, State(state): State<AppState>,
Json(req): Json<CreateSavedSearchRequest>, Json(req): Json<CreateSavedSearchRequest>,
) -> Result<Json<SavedSearchResponse>, ApiError> { ) -> Result<Json<SavedSearchResponse>, ApiError> {
let name_len = req.name.chars().count();
if name_len == 0 || name_len > 255 {
return Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(
"name must be 1-255 characters".into(),
),
));
}
if req.query.is_empty() || req.query.len() > 2048 {
return Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(
"query must be 1-2048 bytes".into(),
),
));
}
if let Some(ref sort) = req.sort_order
&& !VALID_SORT_ORDERS.contains(&sort.as_str()) {
return Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(format!(
"sort_order must be one of: {}",
VALID_SORT_ORDERS.join(", ")
)),
));
}
let id = uuid::Uuid::now_v7(); let id = uuid::Uuid::now_v7();
state state
.storage .storage

View file

@ -47,6 +47,13 @@ pub async fn add_subtitle(
), ),
)); ));
} }
if req
.language
.as_ref()
.is_some_and(|l| l.is_empty() || l.len() > 64)
{
return Err(ApiError::bad_request("language must be 1-64 bytes"));
}
let subtitle = Subtitle { let subtitle = Subtitle {
id: Uuid::now_v7(), id: Uuid::now_v7(),
media_id: MediaId(id), media_id: MediaId(id),

View file

@ -16,6 +16,9 @@ pub async fn start_transcode(
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
Json(req): Json<CreateTranscodeRequest>, Json(req): Json<CreateTranscodeRequest>,
) -> Result<Json<serde_json::Value>, ApiError> { ) -> Result<Json<serde_json::Value>, ApiError> {
if req.profile.is_empty() || req.profile.len() > 255 {
return Err(ApiError::bad_request("profile must be 1-255 bytes"));
}
let job_id = state let job_id = state
.job_queue .job_queue
.submit(pinakes_core::jobs::JobKind::Transcode { .submit(pinakes_core::jobs::JobKind::Transcode {