fix(xfdf): escape paired line-ending names in exported XFDF

The head and tail attributes were written from the annotation's /LE names
without XML escaping, so a name holding a quote or angle bracket could add
attributes or elements, including field values, to the exported file.
This commit is contained in:
aiongg
2026-10-22 17:57:38 +00:00
parent 241c5512d6
commit 93436d41b0
2 changed files with 24 additions and 1 deletions
+1 -1
View File
@@ -354,7 +354,7 @@ fn xfdf_annots(doc: &Document, out: &mut String) {
&& le.len() == 2
{
let name = |o: &Object| o.as_name().map(|x| String::from_utf8_lossy(x).into_owned()).unwrap_or_else(|| "None".into());
let _ = write!(attrs, " head=\"{}\" tail=\"{}\"", name(&le[0]), name(&le[1]));
let _ = write!(attrs, " head=\"{}\" tail=\"{}\"", esc(&name(&le[0])), esc(&name(&le[1])));
}
// A callout: its leader path, text-box inset, intent and (single-name) line ending.
let cl = nums_of(doc, d.get(b"CL"));
+23
View File
@@ -1758,3 +1758,26 @@ fn an_imported_redaction_rect_with_a_non_number_is_not_used() {
let mark = imported_redact_mark(fdf);
assert!(mark.get(b"QuadPoints").is_none(), "quads were drawn from a malformed /Rect: {mark:?}");
}
/// Line-ending names are attribute values: a name holding a quote or angle bracket cannot add
/// attributes or elements to the exported XFDF.
#[test]
fn xfdf_line_ending_names_are_escaped() {
let mut doc = blank();
let page = pdfcraft_model::pages(&doc)[0].obj;
let hostile = b"a\" tail=\"x\"><field name=\"Name\"><value>pwned</value></field><b".to_vec();
let mut line = Dict::new();
line.set(b"Type".to_vec(), Object::name("Annot"));
line.set(b"Subtype".to_vec(), Object::name("Line"));
line.set(b"Rect".to_vec(), Object::Array(vec![10.into(), 20.into(), 30.into(), 40.into()]));
line.set(b"L".to_vec(), Object::Array(vec![10.into(), 20.into(), 30.into(), 40.into()]));
line.set(b"LE".to_vec(), Object::Array(vec![Object::Name(hostile.clone()), Object::Name(hostile.clone())]));
let line = doc.add(line);
doc.update_dict(page, |d| d.set(b"Annots".to_vec(), Object::Array(vec![Object::Ref(line)]))).unwrap();
let xml = export_xfdf(&doc, true, false, "form.pdf");
let parsed = roxmltree::Document::parse(&xml).unwrap();
assert!(!parsed.descendants().any(|n| n.has_tag_name("field")), "{xml}");
let el = parsed.descendants().find(|n| n.has_tag_name("line")).unwrap();
let want = String::from_utf8(hostile).unwrap();
assert_eq!((el.attribute("head"), el.attribute("tail")), (Some(want.as_str()), Some(want.as_str())));
}