From 7a6e9350fead617af8db77ca334306c0fcf000ad Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 18 Jun 2026 13:38:03 +0800 Subject: [PATCH] fix(image): reject undecodable images with 422; cap raw file size at 20MB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - decode 失败(WebP 与其他格式)不再降级返回原始字节,防止构造的畸形文件以 图片 content-type 返回任意内容(M3) - 原始分支读前查 metadata,超 20MB 返回 413,避免超大文件撑爆内存 --- src/api/image.rs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/api/image.rs b/src/api/image.rs index 3ff8a55..9062baf 100644 --- a/src/api/image.rs +++ b/src/api/image.rs @@ -350,9 +350,10 @@ fn process_image_blocking( img } Err(e) => { - tracing::warn!("WebP decode failed ({}), returning raw bytes", e); - let ct = content_type(original_format); - return Ok((data, ct)); + // decode 失败不再降级返回原始字节(可能是构造的畸形文件,配合 nosniff + // 构成内容混淆面),直接报错让上层返回 422(M3)。 + tracing::warn!("WebP decode failed ({}), rejecting", e); + return Err(StatusCode::UNPROCESSABLE_ENTITY); } } } else { @@ -362,9 +363,8 @@ fn process_image_blocking( match reader.decode() { Ok(img) => img, Err(e) => { - tracing::warn!("Image decode failed ({}), returning raw bytes", e); - let ct = content_type(original_format); - return Ok((data, ct)); + tracing::warn!("Image decode failed ({}), rejecting", e); + return Err(StatusCode::UNPROCESSABLE_ENTITY); } } }; @@ -465,11 +465,18 @@ pub async fn serve_image( // No processing params: return raw file with long-lived cache headers. if params.is_empty() { - return match tokio::fs::read(&file_path).await { - Ok(data) => { - let ct = content_type(detect_format(&path)); - image_response(Bytes::from(data), ct, "public, max-age=31536000, immutable", &headers) - } + // 原始分支也限制大小,避免读取超大文件撑爆内存(M3)。上限 20MB + // 覆盖正常上传图(上传侧 MAX_FILE_SIZE=5MB),拒绝异常大文件。 + const MAX_RAW_BYTES: u64 = 20 * 1024 * 1024; + return match tokio::fs::metadata(&file_path).await { + Ok(meta) if meta.len() > MAX_RAW_BYTES => StatusCode::PAYLOAD_TOO_LARGE.into_response(), + Ok(_) => match tokio::fs::read(&file_path).await { + Ok(data) => { + let ct = content_type(detect_format(&path)); + image_response(Bytes::from(data), ct, "public, max-age=31536000, immutable", &headers) + } + Err(_) => StatusCode::NOT_FOUND.into_response(), + }, Err(_) => StatusCode::NOT_FOUND.into_response(), }; }