From 1ead450e3394c2d851afd30a8e7230d7877e4504 Mon Sep 17 00:00:00 2001 From: Linghong Date: Sat, 18 Jul 2026 09:04:03 +0000 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=89=8B=E5=8A=A8=E5=8E=BB?= =?UTF-8?q?=E8=83=8C=E6=99=AF=E8=BE=93=E5=85=A5=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加手动去背景源对象的 OSS 大小和类型预检。 使用 Range 文件头和图片魔数校验,避免非法对象进入 BgFilter retry。 补充内容类型、魔数匹配和调用顺序测试。 --- .../crates/api-server/src/editor_project.rs | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index 4a5c07bfc..810826060 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -2987,6 +2987,7 @@ pub(crate) async fn remove_editor_image_background_for_owner( payload.source_image_src.as_str(), ) .await?; + validate_editor_background_removal_source(state, source_object_key.as_str()).await?; let removed = request_editor_background_removal_image_with_retry(state, source_object_key.as_str()) .await?; @@ -3083,6 +3084,121 @@ struct EditorBackgroundRemovalImage { height: u32, } +async fn validate_editor_background_removal_source( + state: &AppState, + source_object_key: &str, +) -> Result<(), AppError> { + let oss_client = state.oss_client().ok_or_else(|| { + AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({ + "provider": "aliyun-oss", + "reason": "OSS 未完成环境变量配置", + })) + })?; + let http_client = reqwest::Client::new(); + let head = oss_client + .head_object( + &http_client, + OssHeadObjectRequest { + object_key: source_object_key.to_string(), + }, + ) + .await + .map_err(|error| map_oss_error(error, "aliyun-oss"))?; + if head.content_length > EDITOR_REFERENCE_IMAGE_MAX_SIZE_BYTES { + return Err(editor_reference_image_too_large()); + } + + let content_type = + normalize_editor_background_removal_source_mime_type(head.content_type.as_deref()) + .ok_or_else(|| { + editor_background_removal_source_unsupported( + "图片参考图的 Content-Type 不是支持的图片类型。", + ) + })?; + let signed_url = sign_editor_private_object_read_url( + state, + source_object_key, + EDITOR_MATTING_SOURCE_URL_EXPIRE_SECONDS, + )?; + let probe_end = EDITOR_BACKGROUND_REMOVAL_SOURCE_PROBE_BYTES.saturating_sub(1); + let response = http_client + .get(signed_url) + .header(reqwest::header::RANGE, format!("bytes=0-{probe_end}")) + .send() + .await + .map_err(|error| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "aliyun-oss", + "message": format!("读取图片参考图文件头失败:{error}"), + })) + })?; + if response.status() != StatusCode::PARTIAL_CONTENT { + return Err( + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "aliyun-oss", + "message": "OSS 未按 Range 返回图片文件头。", + "status": response.status().as_u16(), + })), + ); + } + if response + .content_length() + .is_none_or(|size| size > EDITOR_BACKGROUND_REMOVAL_SOURCE_PROBE_BYTES) + { + return Err( + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "aliyun-oss", + "message": "OSS 返回的图片文件头大小异常。", + })), + ); + } + let probe = response.bytes().await.map_err(|error| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "aliyun-oss", + "message": format!("读取图片参考图文件头失败:{error}"), + })) + })?; + validate_editor_background_removal_source_content(content_type, probe.as_ref()) +} + +fn validate_editor_background_removal_source_content( + content_type: &str, + bytes: &[u8], +) -> Result<(), AppError> { + let detected_type = infer_editor_reference_image_mime_type(bytes).ok_or_else(|| { + editor_background_removal_source_unsupported("图片参考图文件魔数不是支持的图片类型。") + })?; + if detected_type != content_type { + return Err(editor_background_removal_source_unsupported( + "图片参考图的 Content-Type 与文件魔数不一致。", + )); + } + Ok(()) +} + +fn normalize_editor_background_removal_source_mime_type( + content_type: Option<&str>, +) -> Option<&'static str> { + let mime_type = content_type?.split(';').next()?.trim(); + if mime_type.eq_ignore_ascii_case("image/png") { + Some("image/png") + } else if mime_type.eq_ignore_ascii_case("image/jpeg") { + Some("image/jpeg") + } else if mime_type.eq_ignore_ascii_case("image/webp") { + Some("image/webp") + } else { + None + } +} + +fn editor_background_removal_source_unsupported(message: &str) -> AppError { + AppError::from_status(StatusCode::UNSUPPORTED_MEDIA_TYPE).with_details(json!({ + "provider": "editor-reference-image", + "field": "sourceImageSrc", + "message": message, + })) +} + async fn request_editor_background_removal_image_with_retry( state: &AppState, source_object_key: &str, @@ -7220,6 +7336,7 @@ async fn persist_editor_provider_source_resource( const EDITOR_REFERENCE_IMAGE_READ_EXPIRE_SECONDS: u64 = 300; const EDITOR_REFERENCE_IMAGE_MAX_SIZE_BYTES: u64 = 32 * 1024 * 1024; +const EDITOR_BACKGROUND_REMOVAL_SOURCE_PROBE_BYTES: u64 = 16; const EDITOR_BACKGROUND_REMOVAL_MAX_RESPONSE_BYTES: usize = 32 * 1024 * 1024; const EDITOR_BACKGROUND_REMOVAL_MAX_IMAGE_DIMENSION: u32 = 8192; @@ -9902,6 +10019,49 @@ mod tests { assert_eq!(infer_editor_reference_image_mime_type(b"not image"), None); } + #[test] + fn editor_background_removal_source_requires_supported_content_type_and_matching_magic() { + assert_eq!( + normalize_editor_background_removal_source_mime_type(Some("IMAGE/PNG; charset=utf-8")), + Some("image/png") + ); + assert_eq!( + normalize_editor_background_removal_source_mime_type(Some("image/gif")), + None + ); + assert_eq!( + normalize_editor_background_removal_source_mime_type(Some("application/pdf")), + None + ); + assert_eq!( + normalize_editor_background_removal_source_mime_type(None), + None + ); + + assert!( + validate_editor_background_removal_source_content( + "image/png", + b"\x89PNG\r\n\x1a\nrest" + ) + .is_ok() + ); + assert_eq!( + validate_editor_background_removal_source_content( + "image/png", + &[0xff, 0xd8, 0xff, 0xe0] + ) + .expect_err("mismatched image content should be rejected") + .status_code(), + StatusCode::UNSUPPORTED_MEDIA_TYPE + ); + assert_eq!( + validate_editor_background_removal_source_content("image/png", b"not image") + .expect_err("non-image content should be rejected") + .status_code(), + StatusCode::UNSUPPORTED_MEDIA_TYPE + ); + } + #[test] fn editor_character_postprocess_fallback_response_keeps_source_and_warning() { let response = EditorImageGenerationResponse { @@ -10610,6 +10770,7 @@ mod tests { &[ "caller.report_processing_phase(state).await?", "resolve_editor_reference_object_key_for_owner", + "validate_editor_background_removal_source", "request_editor_background_removal_image_with_retry", ], );