DOCX: cap the depth of header/footer parts that reference further headers/footers (#1322) (#1415)

This commit is contained in:
MarekSpodBiedry
2026-10-14 19:06:52 -04:00
committed by GitHub
parent 81c52fecc0
commit 1024fc471b
2 changed files with 35 additions and 1 deletions
+8 -1
View File
@@ -35,6 +35,8 @@ use story::StoryCtx;
/// Most parts (headers, notes, comments, text boxes) we create from one file.
const MAX_PARTS: usize = 50_000;
/// Deepest chain of headers/footers whose own sections reference further headers/footers.
const MAX_HF_DEPTH: usize = 4;
/// Most graphic work (items plus path segments, see `story::graphic_work`) built for one file's
/// charts and diagrams; later ones are left empty.
const MAX_GRAPHIC_WORK: usize = 2_000_000;
@@ -64,6 +66,8 @@ pub(crate) struct Reader<'p> {
pub moves: MoveCtx,
media_by_path: HashMap<String, String>,
hf_by_path: HashMap<String, u32>,
/// Header/footer parts being read right now (see [`MAX_HF_DEPTH`]).
hf_depth: usize,
pub footnotes: HashMap<i64, u32>,
pub endnotes: HashMap<i64, u32>,
/// The note being read (kind, part id): its `w:footnoteRef` / `w:endnoteRef` mark is a
@@ -141,6 +145,7 @@ pub(crate) fn read_opened(pkg: &Package) -> Result<Document, DocxError> {
moves: MoveCtx::default(),
media_by_path: HashMap::new(),
hf_by_path: HashMap::new(),
hf_depth: 0,
footnotes: HashMap::new(),
endnotes: HashMap::new(),
current_note: None,
@@ -643,7 +648,7 @@ impl Reader<'_> {
if let Some(id) = self.hf_by_path.get(path) {
return Some(*id);
}
if self.doc.parts.len() >= MAX_PARTS {
if self.doc.parts.len() >= MAX_PARTS || self.hf_depth >= MAX_HF_DEPTH {
return None;
}
// Reserve the id first so a (malformed) self-reference can't recurse.
@@ -658,7 +663,9 @@ impl Reader<'_> {
let mut sc = StoryCtx::default();
sc.in_hf = true;
let mut blocks = Blocks::new();
self.hf_depth += 1;
self.read_blocks(&mut sc, &root, &rels, &mut blocks, 0);
self.hf_depth = self.hf_depth.saturating_sub(1);
self.flush_pending(&mut sc, &mut blocks);
if let Some(p) = self.doc.parts.get_mut(&id)
&& !blocks.is_empty()
+27
View File
@@ -238,3 +238,30 @@ proptest! {
let _ = read(&z);
}
}
#[test]
fn long_header_reference_chain_is_cut_off() {
const HDR_REL: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header";
const RELS_NS: &str = "http://schemas.openxmlformats.org/package/2006/relationships";
let rels_for = |next: usize| {
let rel = format!(r#"<Relationship Id="rId1" Type="{HDR_REL}" Target="header{next}.xml"/>"#);
format!(r#"<Relationships xmlns="{RELS_NS}">{rel}</Relationships>"#)
};
let sect = r#"<w:sectPr><w:headerReference w:type="default" r:id="rId1"/></w:sectPr>"#;
let doc = format!("<w:document {W_NS}><w:body><w:p><w:r><w:t>body</w:t></w:r></w:p>{sect}</w:body></w:document>");
// header0 -> header1 -> ... -> header59: every header's last paragraph holds a section that points at the next one.
let mut entries: Vec<(String, Vec<u8>)> = vec![
("_rels/.rels".into(), ROOT_RELS.as_bytes().to_vec()),
("word/document.xml".into(), doc.into_bytes()),
("word/_rels/document.xml.rels".into(), rels_for(0).into_bytes()),
];
for i in 0..60 {
let hdr = format!("<w:hdr {W_NS}><w:p><w:pPr>{sect}</w:pPr><w:r><w:t>h{i}</w:t></w:r></w:p></w:hdr>");
entries.push((format!("word/header{i}.xml"), hdr.into_bytes()));
entries.push((format!("word/_rels/header{i}.xml.rels"), rels_for(i + 1).into_bytes()));
}
let refs: Vec<(&str, &[u8])> = entries.iter().map(|(n, b)| (n.as_str(), b.as_slice())).collect();
let d = read(&zip(&refs)).unwrap();
assert!(!d.parts.is_empty());
assert!(d.parts.len() <= 8, "followed {} chained headers", d.parts.len());
}