From 457f7069fda14fbbf04da365bda2881d8d9711db Mon Sep 17 00:00:00 2001 From: fluorine Date: Fri, 23 Oct 2026 16:35:46 -0400 Subject: [PATCH] DOCX strict parse: reject an undefined entity and markup after the root Co-Authored-By: Claude Sonnet 5.5 --- crates/docx/src/read/embed.rs | 2 +- crates/docx/src/xml.rs | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/crates/docx/src/read/embed.rs b/crates/docx/src/read/embed.rs index ce4eae7ed..1cc97bb03 100644 --- a/crates/docx/src/read/embed.rs +++ b/crates/docx/src/read/embed.rs @@ -395,7 +395,7 @@ mod tests { // A hyperlink whose relationship id is written without the prefix: the reader resolves // it (the alias `El::attr` accepts), so the part it names is not carried over twice. let named = package( - r#"link"#, + r#"link"#, &[("rIdDoc1", "keep.bin")], &[("word/keep.bin", b"kept")], ); diff --git a/crates/docx/src/xml.rs b/crates/docx/src/xml.rs index d1aa38a91..6fd1db7e9 100644 --- a/crates/docx/src/xml.rs +++ b/crates/docx/src/xml.rs @@ -346,6 +346,9 @@ fn parse_with(bytes: &[u8], extra: &[(&'static str, &str)], strict: bool) -> Res }; match ev { Event::Start(ref e) | Event::Empty(ref e) => { + if strict && root.is_some() && stack.is_empty() { + return Err(DocxError::Xml("more than one root element".into())); + } count += 1; if count > MAX_ELEMENTS { return Err(DocxError::Limit("too many XML elements".into())); @@ -433,15 +436,17 @@ fn parse_with(bytes: &[u8], extra: &[(&'static str, &str)], strict: bool) -> Res } Err(_) => None, }; - if let Some(c) = ch { - push_text(top, &c); + match ch { + Some(c) => push_text(top, &c), + None if strict => return Err(DocxError::Xml("an entity is not defined".into())), + None => {} } } } Event::Eof => break, _ => {} } - if root.is_some() && stack.is_empty() { + if !strict && root.is_some() && stack.is_empty() { break; } } @@ -567,6 +572,22 @@ pub fn body_ns() -> Vec<(String, String)> { mod tests { use super::*; + #[test] + fn strict_parsing_rejects_a_dropped_entity_and_trailing_markup() { + let bad = [&b"before &unknown; after"[..], b"", b""]; + for xml in bad { + assert!(parse_strict(xml).is_err(), "{}", String::from_utf8_lossy(xml)); + } + assert!( + parse_strict( + b"x & y +" + ) + .is_ok() + ); + assert!(parse(b"").is_ok(), "lenient parsing still stops at the root"); + } + #[test] fn a_relationship_attribute_is_the_namespaced_one_or_the_alias_of_one() { // The relationship namespace, and VML's `o:relid`.