diff --git a/crates/pinakes-server/src/routes/enrichment.rs b/crates/pinakes-server/src/routes/enrichment.rs index 0229d55..5b93b3f 100644 --- a/crates/pinakes-server/src/routes/enrichment.rs +++ b/crates/pinakes-server/src/routes/enrichment.rs @@ -42,6 +42,13 @@ pub async fn batch_enrich( State(state): State, Json(req): Json, // Reuse: has media_ids field ) -> Result, ApiError> { + if req.media_ids.is_empty() || req.media_ids.len() > 1000 { + return Err(ApiError( + pinakes_core::error::PinakesError::InvalidOperation( + "media_ids must contain 1-1000 items".into(), + ), + )); + } let media_ids: Vec = req.media_ids.into_iter().map(MediaId).collect(); let job_id = state diff --git a/crates/pinakes-server/src/routes/social.rs b/crates/pinakes-server/src/routes/social.rs index f5bc17a..116146b 100644 --- a/crates/pinakes-server/src/routes/social.rs +++ b/crates/pinakes-server/src/routes/social.rs @@ -40,6 +40,17 @@ pub async fn rate_media( ), )); } + if req + .review_text + .as_ref() + .is_some_and(|t| t.chars().count() > 10_000) + { + return Err(ApiError( + pinakes_core::error::PinakesError::InvalidOperation( + "review_text must not exceed 10000 characters".into(), + ), + )); + } let user_id = resolve_user_id(&state.storage, &username).await?; let rating = state .storage @@ -139,6 +150,13 @@ pub async fn create_share_link( Extension(username): Extension, Json(req): Json, ) -> Result, ApiError> { + if req.password.as_ref().is_some_and(|p| p.len() > 1024) { + return Err(ApiError( + pinakes_core::error::PinakesError::InvalidOperation( + "password must not exceed 1024 bytes".into(), + ), + )); + } let user_id = resolve_user_id(&state.storage, &username).await?; let token = uuid::Uuid::now_v7().to_string().replace('-', ""); let password_hash = match req.password.as_ref() { @@ -178,6 +196,13 @@ pub async fn access_shared_media( Path(token): Path, Query(query): Query, ) -> Result, ApiError> { + if query.password.as_ref().is_some_and(|p| p.len() > 1024) { + return Err(ApiError( + pinakes_core::error::PinakesError::InvalidOperation( + "password must not exceed 1024 bytes".into(), + ), + )); + } let link = state.storage.get_share_link(&token).await?; // Check expiration if let Some(expires) = link.expires_at diff --git a/crates/pinakes-server/src/routes/users.rs b/crates/pinakes-server/src/routes/users.rs index 65dfbd2..e97e8a5 100644 --- a/crates/pinakes-server/src/routes/users.rs +++ b/crates/pinakes-server/src/routes/users.rs @@ -161,12 +161,28 @@ pub async fn get_user_libraries( )) } +fn validate_root_path(path: &str) -> Result<(), ApiError> { + if path.is_empty() || path.len() > 4096 { + return Err(ApiError::bad_request("root_path must be 1-4096 bytes")); + } + if !path.starts_with('/') { + return Err(ApiError::bad_request("root_path must be an absolute path")); + } + if path.split('/').any(|segment| segment == "..") { + return Err(ApiError::bad_request( + "root_path must not contain '..' traversal components", + )); + } + Ok(()) +} + /// Grant library access to a user (admin only) pub async fn grant_library_access( State(state): State, Path(id): Path, Json(req): Json, ) -> Result, ApiError> { + validate_root_path(&req.root_path)?; let user_id: UserId = id.parse::().map(UserId::from).map_err(|_| { ApiError(pinakes_core::error::PinakesError::InvalidOperation( @@ -191,6 +207,7 @@ pub async fn revoke_library_access( Path(id): Path, Json(req): Json, ) -> Result, ApiError> { + validate_root_path(&req.root_path)?; let user_id: UserId = id.parse::().map(UserId::from).map_err(|_| { ApiError(pinakes_core::error::PinakesError::InvalidOperation(