mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-29 12:56:47 -06:00
Check SVG nesting depth after DTD entity expansion (#1177)
Fixes #1081 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8ab0de2690
commit
35f5faa2e6
@@ -74,7 +74,7 @@ fn embedded(used: &Used, limits: &Limits, data: &[u8], opts: &Options) -> Option
|
||||
let left = limits.embedded.saturating_sub(used.bytes.load(Ordering::Relaxed));
|
||||
let plain = inflate(data, left).ok()?;
|
||||
used.bytes.fetch_add(plain.len(), Ordering::Relaxed);
|
||||
if super::nesting(&plain, super::MAX_NESTING) > super::MAX_NESTING {
|
||||
if super::too_deep(&plain) {
|
||||
return None;
|
||||
}
|
||||
used.depth.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
@@ -166,3 +166,18 @@ fn nesting_is_measured_before_parsing() {
|
||||
let p = d.first_page().unwrap();
|
||||
assert!(import_svg(&mut d, &svg(&deep), p, 0.0, 0.0, "x").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entity_expansion_cannot_bypass_the_nesting_limit() {
|
||||
// An internal-DTD entity whose value nests 300 deep, referenced inside a body nesting 300 deep:
|
||||
// the parsed tree is ~600 deep although the raw bytes never are.
|
||||
let inner = format!("{}{}", "<g>".repeat(300), "</g>".repeat(300));
|
||||
let body = format!("{}&deep;{}", "<g>".repeat(300), "</g>".repeat(300));
|
||||
let data = format!(
|
||||
"<?xml version=\"1.0\"?><!DOCTYPE svg [<!ENTITY deep \"{inner}\">]><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"10\" height=\"10\">{body}</svg>"
|
||||
);
|
||||
let mut d = Document::new("t");
|
||||
let p = d.first_page().unwrap();
|
||||
let err = import_svg(&mut d, data.as_bytes(), p, 0.0, 0.0, "x").err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(err.contains("nested more than 512 deep"), "{err:?}");
|
||||
}
|
||||
|
||||
@@ -672,6 +672,42 @@ pub(crate) fn nesting(data: &[u8], limit: usize) -> usize {
|
||||
max
|
||||
}
|
||||
|
||||
/// Whether `data` nests elements more than [`MAX_NESTING`] deep: the byte scan first, then the
|
||||
/// parsed tree (walked without recursion), which also counts what DTD entity references expand to.
|
||||
pub(crate) fn too_deep(data: &[u8]) -> bool {
|
||||
if nesting(data, MAX_NESTING) > MAX_NESTING {
|
||||
return true;
|
||||
}
|
||||
let Ok(text) = std::str::from_utf8(data) else { return false };
|
||||
let opt = usvg::roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() };
|
||||
let Ok(xml) = usvg::roxmltree::Document::parse_with_options(text, opt) else { return false };
|
||||
// Depth-first by sibling and parent links; the root is depth 0, each child one deeper.
|
||||
let (mut node, mut depth) = (xml.root(), 0usize);
|
||||
loop {
|
||||
if let Some(child) = node.first_child() {
|
||||
node = child;
|
||||
depth += 1;
|
||||
if depth > MAX_NESTING {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
loop {
|
||||
if let Some(next) = node.next_sibling() {
|
||||
node = next;
|
||||
break;
|
||||
}
|
||||
match node.parent() {
|
||||
Some(up) if depth > 0 => {
|
||||
node = up;
|
||||
depth -= 1;
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn options(data: &[u8], limits: budget::Limits) -> usvg::Options<'static> {
|
||||
// Only data URIs, within budgets: an opened or pasted SVG never pulls other files off the disk
|
||||
// and never expands without limit.
|
||||
@@ -728,7 +764,7 @@ fn import_within(
|
||||
) -> Result<Imported, ImportError> {
|
||||
// SVGZ is unpacked here, within a budget, so every check below sees the real document.
|
||||
let data = budget::document(data, &limits).map_err(ImportError)?;
|
||||
if nesting(&data, MAX_NESTING) > MAX_NESTING {
|
||||
if too_deep(&data) {
|
||||
return Err(ImportError(format!("elements nested more than {MAX_NESTING} deep")));
|
||||
}
|
||||
let tree = usvg::Tree::from_data(&data, &options(&data, limits)).map_err(|e| ImportError(e.to_string()))?;
|
||||
|
||||
Reference in New Issue
Block a user