修复手动去背景输入校验

增加手动去背景源对象的 OSS 大小和类型预检。

使用 Range 文件头和图片魔数校验,避免非法对象进入 BgFilter retry。

补充内容类型、魔数匹配和调用顺序测试。
This commit is contained in:
2026-07-18 09:04:03 +00:00
parent 2fe0a059c6
commit 1ead450e33
@@ -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",
],
);