Security: Affinity import and brush expansion bounds (#3887)

* Audit: Affinity import (spot colour chain, zero-width records, corner radii)

* Audit: Affinity layer names are cut at 1024 characters

* Audit: brush expansion bounds (masks, library parsing, flattening), pressure profile lookup

* Brush: reach a node's mask through its extras
This commit is contained in:
Dylan M. Taylor
2026-10-23 22:43:42 -04:00
committed by GitHub
parent c8c005b7f3
commit 4c82749b08
8 changed files with 74 additions and 10 deletions
+7 -1
View File
@@ -215,7 +215,13 @@ fn anchors(records: &[Value]) -> Option<Vec<Anchor>> {
fn subpath(anchors: &[Anchor], closed: bool, radii: Option<&Vec<(usize, f64)>>) -> Option<SubPath> {
let first = anchors.first()?;
if let Some(radii) = radii.filter(|r| !r.is_empty()) {
let corner = |a: &Anchor| radii.iter().filter(|(rec, _)| *rec == a.record).map(|(_, r)| *r).fold(0.0, f64::max);
// The largest radius of each record, once (not a scan of all of them for each anchor).
let mut largest: std::collections::HashMap<usize, f64> = std::collections::HashMap::new();
for (rec, r) in radii {
let e = largest.entry(*rec).or_insert(0.0);
*e = e.max(*r);
}
let corner = |a: &Anchor| largest.get(&a.record).copied().unwrap_or(0.0);
let polyline = anchors.iter().all(|a| a.inh.is_none_or(|h| h == a.p) && a.out.is_none_or(|h| h == a.p));
if polyline {
let mut pts: Vec<(Point, f64)> = anchors.iter().map(|a| (a.p, corner(a))).collect();
+5 -1
View File
@@ -517,6 +517,9 @@ struct ReadAt {
/// Read a document whose spreads are placed by `base` (for embedded documents, nested `depth`
/// deep). What it uses is taken from `budget`, also when it fails part of the way.
/// The most characters of a layer's name kept.
const MAX_NAME_CHARS: usize = 1024;
fn read_at(bytes: &[u8], limits: Limits, budget: &mut Budget, base: Affine, depth: usize) -> ReadAt {
let mut archive = match Archive::open(bytes, limits) {
Ok(a) => a,
@@ -692,7 +695,8 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
let local = s.floats::<6>(id, b"Xfrm").map(Affine::from_xfrm).unwrap_or(Affine::IDENTITY);
let world = local.then(parent);
let class = s.class(id).unwrap_or(Tag(0));
let name = s.str(id, b"Desc").unwrap_or_default().to_string();
// A layer name is short; a long one is cut, as every visit of a shared object keeps a copy.
let name: String = s.str(id, b"Desc").unwrap_or_default().chars().take(MAX_NAME_CHARS).collect();
let visible = s.bool(id, b"Visi").unwrap_or(true);
let locked = s.bool(id, b"Edtb").is_some_and(|e| !e);
let opacity = s.f64(id, b"Opac").map_or(1.0, |o| o.clamp(0.0, 1.0));
+12 -1
View File
@@ -139,6 +139,14 @@ fn floats(v: &Value) -> Option<Vec<f64>> {
/// A colour object (`RGBA`, `HSLA`, `CMYK`, `GRAY`, `LABA`, `Pant`).
pub(crate) fn color(r: &mut Reader, id: ObjId) -> Option<Color> {
color_within(r, id, 0)
}
/// The most spot colours that stand for one another followed.
const MAX_SPOT_CHAIN: u32 = 8;
/// [`color`], `depth` spot colours deep: one that stands for itself (or a ring of them) ends here.
fn color_within(r: &mut Reader, id: ObjId, depth: u32) -> Option<Color> {
let s = r.s;
let class = s.class(id)?;
let col = || -> Option<Vec<f64>> {
@@ -170,7 +178,10 @@ pub(crate) fn color(r: &mut Reader, id: ObjId) -> Option<Color> {
}
b"Pant" => {
r.warn("spot colours (imported as their RGB equivalent)");
return s.obj(id, b"srgb").and_then(|c| color(r, c)).or_else(|| s.obj(id, b"base").and_then(|c| color(r, c)));
if depth >= MAX_SPOT_CHAIN {
return None;
}
return s.obj(id, b"srgb").and_then(|c| color_within(r, c, depth + 1)).or_else(|| s.obj(id, b"base").and_then(|c| color_within(r, c, depth + 1)));
}
b"RegC" => Color::Cmyk { c: 1.0, m: 1.0, y: 1.0, k: 1.0, a: 1.0 },
_ => {
+4
View File
@@ -303,6 +303,10 @@ impl Parser<'_> {
0x2c => {
let n = usize::try_from(self.c.u32()?).map_err(|_| Error::Malformed("array length"))?;
let size = usize::from(self.c.u16()?);
// Records of no bytes take nothing from the data, whatever their number.
if n > 0 && size == 0 {
return Err(Error::Malformed("array of records without bytes"));
}
if n.checked_mul(size).is_none_or(|b| b > self.c.remaining()) {
return Err(Error::Malformed("array longer than its data"));
}