fix(xfa): scripts come only from the XDP's own packets and quote the date pattern (#5987)

The template (and the saved form state) was found anywhere under the XDP root, so a
template-shaped element inside imported datasets could supply the form and its event
scripts. A date field's pattern went into AFDate_FormatEx/KeystrokeEx string literals
unescaped.
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-03 14:33:41 -05:00
1 parent ecdd1e658c
commit 76260bb6a5
4 files changed
+52 -5

No files matched your search

+4 -2
View File
@@ -746,10 +746,12 @@ pub fn parse(xml: &str) -> Result<(Template, Vec<String>), XfaError> {
&& n.tag_name().name() == "template"
&& (n.tag_name().namespace().is_some_and(|ns| ns.contains("xfa-template")) || children(*n, "subform").next().is_some())
};
// The template is a packet of the XDP itself, a child of its root: one nested deeper is
// data (an imported dataset, say), which must not supply the form's scripts.
let template = if is_template(&root) {
root
} else {
root.descendants().find(is_template).ok_or_else(|| XfaError::Malformed("no <template> element".into()))?
root.children().find(is_template).ok_or_else(|| XfaError::Malformed("no <template> element".into()))?
};
let mut warnings = Vec::new();
let sf = children(template, "subform").next().ok_or_else(|| XfaError::Malformed("the template has no root subform".into()))?;
@@ -760,7 +762,7 @@ pub fn parse(xml: &str) -> Result<(Template, Vec<String>), XfaError> {
}
// The form packet beside the template holds the state the form was saved in.
let saved = root
.descendants()
.children()
.find(|n| n.is_element() && n.tag_name().name() == "form" && n.tag_name().namespace().is_some_and(|ns| ns.contains("xfa-form")));
if let Some(saved_root) = saved.and_then(|f| children(f, "subform").next()) {
if let Some(p) = saved_root.attribute("presence") {
+6 -3
View File
@@ -17,7 +17,9 @@ mod strip;
pub use common::jpeg_size;
pub(crate) use common::rounded;
use common::{all_faces, content_hash, empty_form, hex32, jpeg_info, js_action, n, parse_hex32, rgb, rgb_array, standard_font, state_name};
use common::{
all_faces, content_hash, empty_form, hex32, jpeg_info, js_action, js_string, n, parse_hex32, rgb, rgb_array, standard_font, state_name,
};
pub(crate) use strip::USAGE_RIGHTS_REMOVED;
use strip::drop_usage_rights;
pub use strip::{Stripped, XfaShape, existing_layout, strip, xfa_shape};
@@ -205,8 +207,9 @@ impl Emitter<'_> {
}
if let WidgetKind::Date(pattern) = &w.kind {
let mut aa = Dict::new();
aa.set(b"F".to_vec(), js_action(&format!("AFDate_FormatEx(\"{pattern}\");")));
aa.set(b"K".to_vec(), js_action(&format!("AFDate_KeystrokeEx(\"{pattern}\");")));
let pattern = js_string(pattern);
aa.set(b"F".to_vec(), js_action(&format!("AFDate_FormatEx({pattern});")));
aa.set(b"K".to_vec(), js_action(&format!("AFDate_KeystrokeEx({pattern});")));
d.set(b"AA".to_vec(), Object::Dict(aa));
}
}
+32
View File
@@ -192,9 +192,41 @@ pub(super) fn empty_form(doc: &mut Document, w: f64, h: f64) -> Object {
Object::Ref(doc.add(Object::Stream(Stream::from_raw(d, Vec::new()))))
}
/// `s` as a JavaScript string literal, quotes included: a quote, a backslash or a line break in
/// it stays text instead of ending the string and running as code.
pub(super) fn js_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' | '\\' => {
out.push('\\');
out.push(c);
}
c if c.is_control() || c == '\u{2028}' || c == '\u{2029}' => out.push_str(&format!("\\u{:04x}", u32::from(c))),
c => out.push(c),
}
}
out.push('"');
out
}
pub(super) fn js_action(js: &str) -> Object {
let mut a = Dict::new();
a.set(b"S".to_vec(), Object::name("JavaScript"));
a.set(b"JS".to_vec(), Object::String(PdfString::literal(js.as_bytes().to_vec())));
Object::Dict(a)
}
#[cfg(test)]
mod js_string_tests {
use super::js_string;
/// A date pattern that tries to close the string and run code stays one string literal.
#[test]
fn a_pattern_cannot_leave_its_string() {
assert_eq!(js_string("yyyy-mm-dd"), "\"yyyy-mm-dd\"");
assert_eq!(js_string("x\");app.alert(1);//"), "\"x\\\");app.alert(1);//\"");
assert_eq!(js_string("a\\\nb\u{2028}"), "\"a\\\\\\u000ab\\u2028\"");
}
}
+10
View File
@@ -4531,3 +4531,13 @@ fn a_container_is_as_wide_as_a_turned_draw_stands() {
assert!((got - right).abs() < 0.01, "{rotate}: the neighbour stands {got} right of the anchor, not {right}");
}
}
/// Only a template that is a packet of the XDP is the form's: one inside imported data (a
/// dataset holding XML that looks like a template) is not read, and neither are its scripts.
#[test]
fn a_template_inside_the_data_is_not_the_form() {
let nested = "<xdp:xdp xmlns:xdp=\"http://ns.adobe.com/xdp/\"><xfa:datasets xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\"><xfa:data><template xmlns=\"http://www.xfa.org/schema/xfa-template/3.3/\"><subform name=\"evil\"><event activity=\"initialize\"><script>xfa.host.messageBox(1)</script></event></subform></template></xfa:data></xfa:datasets></xdp:xdp>";
assert!(parse(nested).is_err(), "no template packet");
let real = "<xdp:xdp xmlns:xdp=\"http://ns.adobe.com/xdp/\"><template xmlns=\"http://www.xfa.org/schema/xfa-template/3.3/\"><subform name=\"form1\"/></template></xdp:xdp>";
assert_eq!(parse(real).unwrap().0.root.common.name.as_deref(), Some("form1"));
}