mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 14:16:56 -06:00
100 lines
4.2 KiB
Rust
100 lines
4.2 KiB
Rust
//! A cap on what the entities of an SVG's document type declaration expand to. The XML reader
|
|
//! replaces every `&name;` with the entity's text as it reads, with no total limit: a short file
|
|
//! that repeats a large entity in an attribute allocates far more than it holds.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
/// The most text the references to entities may expand to in all (bytes).
|
|
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();
|
|
for (at, _) in svg.match_indices("<!ENTITY") {
|
|
let rest = svg.get(at + "<!ENTITY".len()..).unwrap_or("").trim_start();
|
|
// A parameter entity (`%`) isn't referenced as `&name;`.
|
|
if rest.starts_with('%') {
|
|
continue;
|
|
}
|
|
let Some(end) = rest.find(char::is_whitespace) else { continue };
|
|
let (name, rest) = rest.split_at(end);
|
|
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
|
|
}
|
|
|
|
/// The names of the `&name;` references in `text`.
|
|
fn references(text: &str) -> impl Iterator<Item = &str> {
|
|
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 name = rest.get(..end)?;
|
|
(!name.is_empty() && name.len() <= 256 && !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 {
|
|
if let Some(n) = memo.get(name) {
|
|
return *n;
|
|
}
|
|
let Some(value) = 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));
|
|
}
|
|
memo.insert(name, total);
|
|
total
|
|
}
|
|
|
|
/// Whether the references to the entities `svg` declares expand to more than [`MAX_EXPANDED`]
|
|
/// bytes in all. False for a file that declares none.
|
|
pub(crate) fn expansion_too_large(svg: &str) -> bool {
|
|
let entities = declared(svg);
|
|
if entities.is_empty() {
|
|
return false;
|
|
}
|
|
let mut memo = HashMap::new();
|
|
let mut total = 0usize;
|
|
for name in references(svg) {
|
|
total = total.saturating_add(expanded(name, &entities, &mut memo, 0));
|
|
if total > MAX_EXPANDED {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn a_repeated_large_entity_is_too_large_and_a_namespace_entity_is_not() {
|
|
let big = "x".repeat(1 << 20);
|
|
let refs = "&a;".repeat(20);
|
|
let bomb = format!("<!DOCTYPE svg [<!ENTITY a \"{big}\">]><svg xmlns=\"http://www.w3.org/2000/svg\" id=\"{refs}\"/>");
|
|
assert!(expansion_too_large(&bomb));
|
|
let nested = format!("<!DOCTYPE svg [<!ENTITY a \"{big}\"><!ENTITY b \"{}\">]><svg id=\"{}\"/>", "&a;".repeat(8), "&b;".repeat(8));
|
|
assert!(expansion_too_large(&nested));
|
|
let ns = "<!DOCTYPE svg [<!ENTITY ns_svg \"http://www.w3.org/2000/svg\">]><svg xmlns=\"&ns_svg;\"/>";
|
|
assert!(!expansion_too_large(ns));
|
|
assert!(!expansion_too_large("<svg a=\"& \"/>"));
|
|
// Many `&`s with no `;` after them are read in linear time.
|
|
let ampersands = format!("<!DOCTYPE svg [<!ENTITY a \"x\">]><svg id=\"{}\"/>", "&".repeat(1 << 20));
|
|
let start = std::time::Instant::now();
|
|
assert!(!expansion_too_large(&ersands));
|
|
assert!(start.elapsed() < std::time::Duration::from_secs(5), "{:?}", start.elapsed());
|
|
}
|
|
}
|