From 3739b2863aca9a0a4668ff491f41305246b0f68e Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 24 Oct 2026 04:41:31 -0400 Subject: [PATCH] fix(security): cap COLR layers, PNG glyph dimensions, and layout cache capacity (#1264) * chore: explicit test imports in engine, part 1 (Sonar S2208) Co-Authored-By: Claude Sonnet 5.5 * fix(security): cap COLR layers, PNG glyph dimensions, and layout cache capacity --------- Co-authored-by: Claude Sonnet 5.5 --- crates/engine/src/design_paste_tests.rs | 2 +- crates/text/src/color.rs | 13 +++++++++++-- crates/text/src/lib.rs | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/design_paste_tests.rs b/crates/engine/src/design_paste_tests.rs index 89de80efc..52dc42305 100644 --- a/crates/engine/src/design_paste_tests.rs +++ b/crates/engine/src/design_paste_tests.rs @@ -4,7 +4,7 @@ use base64::Engine as _; use serde_json::json; -use super::*; +use super::{NodeId, Session, Value, params}; use crate::cmd::edit::{CLIP_CLOSE, CLIP_OPEN, design_clipboard_buffer}; const B64: base64::engine::GeneralPurpose = base64::engine::general_purpose::STANDARD; diff --git a/crates/text/src/color.rs b/crates/text/src/color.rs index ca927a949..0eaca1972 100644 --- a/crates/text/src/color.rs +++ b/crates/text/src/color.rs @@ -29,8 +29,10 @@ const MAX_RASTER_BYTES: usize = 32 * 1024 * 1024; const MAX_RASTER_ENTRIES: usize = 8192; /// COLR layer lists kept per face (24 bytes a layer, so a count is enough). const MAX_LAYERS: usize = 4096; +/// Most COLR layers read for one glyph. +const MAX_COLR_LAYERS: usize = 256; /// Widest and tallest PNG strike decoded (strikes are at most a few hundred pixels). -const MAX_PNG_SIDE: u32 = 4096; +const MAX_PNG_SIDE: u32 = 1024; /// Largest colour raster per em (device pixels). Past this the renderer scales one up instead. pub const MAX_PX_PER_EM: f64 = 512.0; @@ -171,6 +173,9 @@ impl Face { let colors = palette.as_ref().map(|p| p.colors()).unwrap_or(&[]); let mut out = Vec::new(); for index in range { + if out.len() >= MAX_COLR_LAYERS { + break; + } let (layer, color_index) = colr.v0_layer(index)?; let rgba = colors.get(usize::from(color_index)).map(|c| [c.red(), c.green(), c.blue(), c.alpha()]).unwrap_or([0, 0, 0, 255]); out.push(ColorLayer { gid: layer.to_u32(), rgba }); @@ -230,7 +235,11 @@ fn decode_png(png: &[u8]) -> Option<(Vec, u32, u32)> { if w == 0 || h == 0 || w > MAX_PNG_SIDE || h > MAX_PNG_SIDE { return None; } - let mut buf = vec![0u8; reader.output_buffer_size()?]; + let buf_size = reader.output_buffer_size()?; + if buf_size > 4 * 1024 * 1024 { + return None; + } + let mut buf = vec![0u8; buf_size]; let info = reader.next_frame(&mut buf).ok()?; let (width, height) = (info.width, info.height); let data = buf.get(..info.buffer_size())?; diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 50cd28dd5..548a7b66a 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -42,7 +42,7 @@ struct LayoutCache { /// Layouts kept before the cache starts over (a page of distinct texts stays cached while it /// renders, pans and zooms). -const LAYOUT_CACHE_MAX: usize = 32_768; +const LAYOUT_CACHE_MAX: usize = 4_096; /// Entries per fingerprint (texts that compare unequal to themselves, e.g. NaN sizes, can't pile up). const BUCKET_MAX: usize = 8;