源图解码加尺寸/分配上限,堵住解压炸弹风险

两处在缩图前直接 image::load_from_memory:背景色过滤的参考图(请求体 data URL)
与阿里云抠图源图。缺省 image::Limits 无尺寸上限、仅 512MiB alloc 兜底,12MB 请求体
即可构造巨幅压缩图在解码阶段撑爆内存,逐帧动画并发时叠加。
改用带 Limits 的 ImageReader(边长 ≤8192、alloc ≤128MiB),与结果解码同口径。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 08:38:07 +00:00
parent f5aa50a68b
commit fa64bfb475
2 changed files with 46 additions and 16 deletions
@@ -32,6 +32,11 @@ const DANGER_MASS_EXCLUSION_RATIO: f64 = 0.005;
const BIN_MASS_FLOOR_RATIO: f64 = 0.001;
/// 分析用缩略图最长边。
const ANALYSIS_MAX_EDGE: u32 = 192;
/// 参考图解码边长上限:源图直接来自请求体 data URL,缺省 `image::Limits` 无尺寸上限、
/// 仅 512MiB alloc 兜底,12MB 请求体即可构造巨幅压缩图(解压炸弹)在缩图前撑爆内存。
const ANALYSIS_SOURCE_MAX_DIMENSION: u32 = 8192;
/// 参考图解码分配上限。与 api-server BgFilter 结果解码口径一致(128MiB)。
const ANALYSIS_SOURCE_MAX_ALLOC_BYTES: u64 = 128 * 1024 * 1024;
/// 不透明图边框背景估计:主色覆盖率达到该值才认为边框是平坦背景。
const BORDER_BACKGROUND_MIN_COVERAGE: f64 = 0.5;
/// 不透明图中与边框背景色 ΔE 不超过该值的像素按背景剔除。
@@ -87,6 +92,23 @@ pub(crate) fn decode_image_data_url(value: &str) -> Option<Vec<u8>> {
.filter(|bytes| !bytes.is_empty())
}
/// 有界解码参考图:套上尺寸 / 分配上限,防止解压炸弹在缩图前撑爆内存。
fn decode_analysis_image_within_limits(bytes: &[u8]) -> Result<image::DynamicImage, String> {
use std::io::Cursor;
let mut reader = image::ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.map_err(|error| format!("参考图无法识别格式:{error}"))?;
let mut limits = image::Limits::default();
limits.max_image_width = Some(ANALYSIS_SOURCE_MAX_DIMENSION);
limits.max_image_height = Some(ANALYSIS_SOURCE_MAX_DIMENSION);
limits.max_alloc = Some(ANALYSIS_SOURCE_MAX_ALLOC_BYTES);
reader.limits(limits);
reader
.decode()
.map_err(|error| format!("参考图无法解码:{error}"))
}
/// 按参考图前景配色过滤候选背景色。`apply_skin_veto` 为 true(角色/动作类)时,检测到皮肤 bin
/// 还会启用 Rule 2/3 皮肤硬否决。失败(图片不可解码、前景不足)返回原因字符串,
/// 调用方应降级为不过滤,而不是阻断生成。
@@ -94,8 +116,7 @@ pub(crate) fn filter_editor_screen_background_colors(
image_bytes: &[u8],
apply_skin_veto: bool,
) -> Result<ScreenBackgroundColorSafetyReport, String> {
let image = image::load_from_memory(image_bytes)
.map_err(|error| format!("参考图无法解码:{error}"))?;
let image = decode_analysis_image_within_limits(image_bytes)?;
let (width, height) = image.dimensions();
if width == 0 || height == 0 {
return Err("参考图尺寸为空".to_string());