pinakes-server: cap batch_enrich size; reject path traversal in library roots
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I42212cdd385921295484d5c1f5fbfeab6a6a6964
This commit is contained in:
parent
c16fcb4a9b
commit
18fda530f2
3 changed files with 49 additions and 0 deletions
|
|
@ -42,6 +42,13 @@ pub async fn batch_enrich(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<BatchDeleteRequest>, // Reuse: has media_ids field
|
Json(req): Json<BatchDeleteRequest>, // Reuse: has media_ids field
|
||||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
) -> Result<Json<serde_json::Value>, 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<MediaId> =
|
let media_ids: Vec<MediaId> =
|
||||||
req.media_ids.into_iter().map(MediaId).collect();
|
req.media_ids.into_iter().map(MediaId).collect();
|
||||||
let job_id = state
|
let job_id = state
|
||||||
|
|
|
||||||
|
|
@ -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 user_id = resolve_user_id(&state.storage, &username).await?;
|
||||||
let rating = state
|
let rating = state
|
||||||
.storage
|
.storage
|
||||||
|
|
@ -139,6 +150,13 @@ pub async fn create_share_link(
|
||||||
Extension(username): Extension<String>,
|
Extension(username): Extension<String>,
|
||||||
Json(req): Json<CreateShareLinkRequest>,
|
Json(req): Json<CreateShareLinkRequest>,
|
||||||
) -> Result<Json<ShareLinkResponse>, ApiError> {
|
) -> Result<Json<ShareLinkResponse>, 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 user_id = resolve_user_id(&state.storage, &username).await?;
|
||||||
let token = uuid::Uuid::now_v7().to_string().replace('-', "");
|
let token = uuid::Uuid::now_v7().to_string().replace('-', "");
|
||||||
let password_hash = match req.password.as_ref() {
|
let password_hash = match req.password.as_ref() {
|
||||||
|
|
@ -178,6 +196,13 @@ pub async fn access_shared_media(
|
||||||
Path(token): Path<String>,
|
Path(token): Path<String>,
|
||||||
Query(query): Query<ShareLinkQuery>,
|
Query(query): Query<ShareLinkQuery>,
|
||||||
) -> Result<Json<MediaResponse>, ApiError> {
|
) -> Result<Json<MediaResponse>, 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?;
|
let link = state.storage.get_share_link(&token).await?;
|
||||||
// Check expiration
|
// Check expiration
|
||||||
if let Some(expires) = link.expires_at
|
if let Some(expires) = link.expires_at
|
||||||
|
|
|
||||||
|
|
@ -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)
|
/// Grant library access to a user (admin only)
|
||||||
pub async fn grant_library_access(
|
pub async fn grant_library_access(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(req): Json<GrantLibraryAccessRequest>,
|
Json(req): Json<GrantLibraryAccessRequest>,
|
||||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||||
|
validate_root_path(&req.root_path)?;
|
||||||
let user_id: UserId =
|
let user_id: UserId =
|
||||||
id.parse::<uuid::Uuid>().map(UserId::from).map_err(|_| {
|
id.parse::<uuid::Uuid>().map(UserId::from).map_err(|_| {
|
||||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
||||||
|
|
@ -191,6 +207,7 @@ pub async fn revoke_library_access(
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(req): Json<RevokeLibraryAccessRequest>,
|
Json(req): Json<RevokeLibraryAccessRequest>,
|
||||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||||
|
validate_root_path(&req.root_path)?;
|
||||||
let user_id: UserId =
|
let user_id: UserId =
|
||||||
id.parse::<uuid::Uuid>().map(UserId::from).map_err(|_| {
|
id.parse::<uuid::Uuid>().map(UserId::from).map_err(|_| {
|
||||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue