fix: add decompression bomb protection and MIME type validation
Set image decoder limits (10000x10000 max dimensions, 256MB max alloc) to prevent crafted images from causing OOM via decompression bombs. Validate data URL MIME types against an allowlist (jpeg/png/webp) before decoding, rejecting non-image content types early.
This commit is contained in:
parent
082b582cb3
commit
3675a2cdc6
1 changed files with 39 additions and 5 deletions
|
|
@ -9,6 +9,15 @@ const MAX_FULL_SIZE: u32 = 1200;
|
||||||
/// Maximum dimension (width or height) for the thumbnail.
|
/// Maximum dimension (width or height) for the thumbnail.
|
||||||
const MAX_THUMBNAIL_SIZE: u32 = 200;
|
const MAX_THUMBNAIL_SIZE: u32 = 200;
|
||||||
|
|
||||||
|
/// Maximum allowed input dimension (width or height) to prevent decompression bombs.
|
||||||
|
const MAX_INPUT_DIMENSION: u32 = 10_000;
|
||||||
|
|
||||||
|
/// Maximum memory the decoder may allocate (256 MB).
|
||||||
|
const MAX_DECODER_ALLOC: u64 = 256 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// Allowed MIME types in data URLs.
|
||||||
|
const ALLOWED_MIMES: &[&str] = &["image/jpeg", "image/png", "image/webp"];
|
||||||
|
|
||||||
/// JPEG quality for the full-size image (0-100).
|
/// JPEG quality for the full-size image (0-100).
|
||||||
const JPEG_QUALITY_FULL: u8 = 85;
|
const JPEG_QUALITY_FULL: u8 = 85;
|
||||||
|
|
||||||
|
|
@ -33,11 +42,17 @@ pub fn process_data_url(data_url: &str) -> anyhow::Result<ProcessedImage> {
|
||||||
|
|
||||||
/// Process raw image bytes (JPEG/PNG/WebP) into resized full + thumbnail JPEGs.
|
/// Process raw image bytes (JPEG/PNG/WebP) into resized full + thumbnail JPEGs.
|
||||||
pub fn process_image_bytes(raw_bytes: &[u8]) -> anyhow::Result<ProcessedImage> {
|
pub fn process_image_bytes(raw_bytes: &[u8]) -> anyhow::Result<ProcessedImage> {
|
||||||
let img = ImageReader::new(Cursor::new(raw_bytes))
|
let mut reader = ImageReader::new(Cursor::new(raw_bytes))
|
||||||
.with_guessed_format()
|
.with_guessed_format()
|
||||||
.context("failed to guess image format")?
|
.context("failed to guess image format")?;
|
||||||
.decode()
|
|
||||||
.context("failed to decode image")?;
|
let mut limits = image::Limits::default();
|
||||||
|
limits.max_image_width = Some(MAX_INPUT_DIMENSION);
|
||||||
|
limits.max_image_height = Some(MAX_INPUT_DIMENSION);
|
||||||
|
limits.max_alloc = Some(MAX_DECODER_ALLOC);
|
||||||
|
reader.limits(limits);
|
||||||
|
|
||||||
|
let img = reader.decode().context("failed to decode image")?;
|
||||||
|
|
||||||
let full = img.resize(
|
let full = img.resize(
|
||||||
MAX_FULL_SIZE,
|
MAX_FULL_SIZE,
|
||||||
|
|
@ -67,10 +82,14 @@ fn decode_data_url(data_url: &str) -> anyhow::Result<Vec<u8>> {
|
||||||
bail!("invalid data URL: missing data: prefix");
|
bail!("invalid data URL: missing data: prefix");
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some((_mime, encoded)) = rest.split_once(',') else {
|
let Some((mime, encoded)) = rest.split_once(',') else {
|
||||||
bail!("invalid data URL: missing comma separator");
|
bail!("invalid data URL: missing comma separator");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !ALLOWED_MIMES.iter().any(|m| mime.contains(m)) {
|
||||||
|
bail!("unsupported image type: {mime}");
|
||||||
|
}
|
||||||
|
|
||||||
base64::engine::general_purpose::STANDARD
|
base64::engine::general_purpose::STANDARD
|
||||||
.decode(encoded.trim())
|
.decode(encoded.trim())
|
||||||
.context("failed to decode base64 image data")
|
.context("failed to decode base64 image data")
|
||||||
|
|
@ -109,4 +128,19 @@ mod tests {
|
||||||
let result = decode_data_url("data:image/png;base64");
|
let result = decode_data_url("data:image/png;base64");
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_data_url_rejects_non_image_mime() {
|
||||||
|
let data = base64::engine::general_purpose::STANDARD.encode(b"hello");
|
||||||
|
let url = format!("data:text/html;base64,{data}");
|
||||||
|
let result = decode_data_url(&url);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(
|
||||||
|
result
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.contains("unsupported image type"),
|
||||||
|
"should reject non-image MIME types"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue