From c972c15d0fd526e070edb514dd479e4e74a20134 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 13:06:25 +0000 Subject: [PATCH] fix(svg): entity expansion estimate counts every declaration and refuses long names --- crates/svg/src/entities.rs | 52 ++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/crates/svg/src/entities.rs b/crates/svg/src/entities.rs index a055e55fd..d1eb4cfef 100644 --- a/crates/svg/src/entities.rs +++ b/crates/svg/src/entities.rs @@ -10,9 +10,14 @@ const MAX_EXPANDED: usize = 16 << 20; /// How deep entities that refer to entities are followed (the reader's own limit is lower). const MAX_NESTING: u32 = 16; -/// The internal general entities declared in `svg`: name → value. -fn declared(svg: &str) -> HashMap<&str, &str> { - let mut map = HashMap::new(); +/// The longest entity name a reference is looked for with ([`references`]). +const MAX_NAME: usize = 256; + +/// The internal general entities declared in `svg`: name → every value declared for it. Text that +/// only looks like a declaration (in a comment, or in another entity's value) is kept too: the +/// largest value counts, so it can only make the estimate larger. +fn declared(svg: &str) -> HashMap<&str, Vec<&str>> { + let mut map: HashMap<&str, Vec<&str>> = HashMap::new(); for (at, _) in svg.match_indices(" HashMap<&str, &str> { let rest = rest.trim_start(); let Some(quote) = rest.chars().next().filter(|q| matches!(q, '"' | '\'')) else { continue }; let Some(value) = rest.get(1..).and_then(|r| r.split_once(quote)).map(|(v, _)| v) else { continue }; - map.entry(name).or_insert(value); + map.entry(name).or_default().push(value); } map } @@ -34,27 +39,31 @@ fn references(text: &str) -> impl Iterator { text.match_indices('&').filter_map(|(at, _)| { let rest = text.get(at + 1..)?; // Look no further than the longest name: a file of `&`s without `;` would be read quadratically. - let end = rest.bytes().take(257).position(|b| b == b';')?; + let end = rest.bytes().take(MAX_NAME + 1).position(|b| b == b';')?; let name = rest.get(..end)?; - (!name.is_empty() && name.len() <= 256 && !name.starts_with('#') && !name.contains(char::is_whitespace)).then_some(name) + (!name.is_empty() && name.len() <= MAX_NAME && !name.starts_with('#') && !name.contains(char::is_whitespace)).then_some(name) }) } /// What entity `name` expands to (bytes), references in its value followed. -fn expanded<'a>(name: &'a str, entities: &HashMap<&'a str, &'a str>, memo: &mut HashMap<&'a str, usize>, depth: u32) -> usize { +fn expanded<'a>(name: &'a str, entities: &HashMap<&'a str, Vec<&'a str>>, memo: &mut HashMap<&'a str, usize>, depth: u32) -> usize { if let Some(n) = memo.get(name) { return *n; } - let Some(value) = entities.get(name) else { return 0 }; + let Some(values) = entities.get(name) else { return 0 }; if depth >= MAX_NESTING { return usize::MAX; } - let mut total = value.len(); - for inner in references(value) { - total = total.saturating_add(expanded(inner, entities, memo, depth + 1)); + let mut most = 0usize; + for value in values { + let mut total = value.len(); + for inner in references(value) { + total = total.saturating_add(expanded(inner, entities, memo, depth + 1)); + } + most = most.max(total); } - memo.insert(name, total); - total + memo.insert(name, most); + most } /// Whether the references to the entities `svg` declares expand to more than [`MAX_EXPANDED`] @@ -64,6 +73,10 @@ pub(crate) fn expansion_too_large(svg: &str) -> bool { if entities.is_empty() { return false; } + // A reference to a longer name isn't looked for: refuse such a declaration instead. + if entities.keys().any(|name| name.len() > MAX_NAME) { + return true; + } let mut memo = HashMap::new(); let mut total = 0usize; for name in references(svg) { @@ -96,4 +109,17 @@ mod tests { assert!(!expansion_too_large(&ersands)); assert!(start.elapsed() < std::time::Duration::from_secs(5), "{:?}", start.elapsed()); } + + #[test] + fn declarations_in_comments_and_long_names_cannot_hide_a_large_entity() { + let big = "x".repeat(1 << 20); + let refs = "&a;".repeat(20); + let commented = format!(" -->]>"); + assert!(expansion_too_large(&commented)); + let in_value = format!("\">]>"); + assert!(expansion_too_large(&in_value)); + let long = "n".repeat(MAX_NAME + 1); + let long_name = format!("]>"); + assert!(expansion_too_large(&long_name)); + } }