JavaScript: limit util.printf widths and output, and array arguments (#770)

util.printf accumulated a conversion's width and precision with unchecked
arithmetic and no maximum, and its output had no bound; the array
arguments of resetForm and the field value/colour setters were read for
as many items as the array's length says, which a sparse array makes
free. Saturate the numbers and refuse widths and precisions above 4096,
output above 1 MiB and arrays longer than 2^20 with a script error,
before anything is allocated from them.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Ikki Nishihara
2026-10-10 15:47:05 -04:00
committed by GitHub
co-authored by Claude Sonnet 5.5
parent 265ddca58c
commit 1b6e144c57
2 changed files with 142 additions and 2 deletions
+24 -2
View File
@@ -247,6 +247,9 @@ fn strings(v: &JsValue, ctx: &mut Context) -> JsResult<Vec<String>> {
{
let a = JsArray::from_object(o.clone())?;
let n = a.length(ctx)?;
if n > MAX_ARRAY_LENGTH {
return Err(error(format!("array length exceeds {MAX_ARRAY_LENGTH}")));
}
let mut out = Vec::new();
for i in 0..n {
let x = a.get(i, ctx)?;
@@ -589,15 +592,23 @@ fn timer(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
fn printf(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
let fmt = text(&arg(args, 0), ctx)?;
let mut out = String::new();
let check_output = |len: usize| -> JsResult<()> {
if len > MAX_PRINTF_OUTPUT_BYTES {
return Err(error(format!("util.printf output exceeds {MAX_PRINTF_OUTPUT_BYTES} bytes")));
}
Ok(())
};
let mut next = 1;
let mut chars = fmt.chars().peekable();
while let Some(c) = chars.next() {
if c != '%' {
check_output(out.len().saturating_add(c.len_utf8()))?;
out.push(c);
continue;
}
if chars.peek() == Some(&'%') {
chars.next();
check_output(out.len().saturating_add(1))?;
out.push('%');
continue;
}
@@ -619,7 +630,7 @@ fn printf(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue>
}
let mut width = 0usize;
while let Some(d) = chars.peek().and_then(|d| d.to_digit(10)) {
width = width * 10 + d as usize;
width = width.saturating_mul(10).saturating_add(d as usize);
chars.next();
}
let mut prec = None;
@@ -627,11 +638,14 @@ fn printf(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue>
chars.next();
let mut p = 0usize;
while let Some(d) = chars.peek().and_then(|d| d.to_digit(10)) {
p = p * 10 + d as usize;
p = p.saturating_mul(10).saturating_add(d as usize);
chars.next();
}
prec = Some(p);
}
if width > MAX_PRINTF_WIDTH_PRECISION || prec.is_some_and(|p| p > MAX_PRINTF_WIDTH_PRECISION) {
return Err(error(format!("util.printf width or precision exceeds {MAX_PRINTF_WIDTH_PRECISION}")));
}
let conv = chars.next().unwrap_or('s');
let v = arg(args, next);
next += 1;
@@ -660,6 +674,7 @@ fn printf(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue>
}
};
let pad = width.saturating_sub(body.chars().count());
check_output(out.len().saturating_add(pad).saturating_add(body.len()))?;
if zero && matches!(conv, 'd' | 'f') {
let (sign, digits) = if body.starts_with(['-', '+', ' ']) { body.split_at(1) } else { ("", body.as_str()) };
out.push_str(sign);
@@ -992,6 +1007,13 @@ fn event_object(ctx: &mut Context, e: &Event) -> JsObject {
/// Longest script, in bytes.
pub(crate) const MAX_SCRIPT_BYTES: usize = 256 * 1024;
/// Largest width or precision accepted by `util.printf`.
const MAX_PRINTF_WIDTH_PRECISION: usize = 4096;
/// Longest `util.printf` output, in bytes.
const MAX_PRINTF_OUTPUT_BYTES: usize = 1 << 20;
/// Longest array read item by item (`resetForm`, a field's `value` or colours). A sparse array's
/// length costs the script nothing but the host one string per item; real lists are tiny.
const MAX_ARRAY_LENGTH: u64 = 1 << 20;
/// Deepest nesting of `(`, `[` and `{`.
const MAX_BRACKET_DEPTH: usize = 64;
/// Longest run of prefix operators (`!`, `~`, `+`, `-`).
+118
View File
@@ -94,6 +94,124 @@ fn util_printf_printd_and_printx() {
assert_eq!(o.value, "1,234,567.89|00042|hi|FF|1.234,50|Jan 5, 2024 09:07|Friday|(555) 123-4567|ABC");
}
fn assert_printf_error(script: &str, message: &str) {
let o = go(script, &Event::doc("Open"));
assert!(o.error.as_deref().is_some_and(|e| e.starts_with(message)), "{script}: {:?}", o.error);
let o = xfa::run_xfa(
script,
&xfa::XfaEvent { activity: "initialize".into(), target: "form1[0]".into(), ..Default::default() },
&xfa::XfaDoc::default(),
&xfa_model::form(),
Limits::default(),
);
assert!(o.error.as_deref().is_some_and(|e| e.starts_with(message)), "XFA: {script}: {:?}", o.error);
}
#[test]
fn util_printf_rejects_large_width() {
assert_printf_error("util.printf('%99999999999d', 1)", "TypeError: util.printf width or precision exceeds 4096");
}
#[test]
fn util_printf_rejects_large_precision() {
assert_printf_error("util.printf('%.99999999999f', 1)", "TypeError: util.printf width or precision exceeds 4096");
}
#[test]
fn util_printf_rejects_overflowing_width() {
assert_printf_error("util.printf('%9999999999999999999999999999999999999999d', 1)", "TypeError: util.printf width or precision exceeds 4096");
}
#[test]
fn util_printf_rejects_overflowing_precision() {
assert_printf_error("util.printf('%.9999999999999999999999999999999999999999f', 1)", "TypeError: util.printf width or precision exceeds 4096");
}
#[test]
fn util_printf_rejects_limits_for_all_conversions() {
for conv in ['d', 'f', 's', 'x'] {
for spec in ["4097", ".4097", "99999999999", ".99999999999"] {
assert_printf_error(&format!("util.printf('%{spec}{conv}', 1)"), "TypeError: util.printf width or precision exceeds 4096");
}
}
}
#[test]
fn util_printf_bounds_total_output() {
for script in [
"util.printf('%4096d'.repeat(300), 1)",
"util.printf('%04096d'.repeat(300), -1)",
"util.printf('aa'.repeat(524288) + 'a')",
"util.printf('é'.repeat(524289))",
"util.printf('%%%%'.repeat(524288) + '%%')",
"util.printf('%s', 'aa'.repeat(524288) + 'a')",
"util.printf('%s%s', 'aa'.repeat(524288), 'b')",
] {
assert_printf_error(script, "TypeError: util.printf output exceeds 1048576 bytes");
}
}
#[test]
fn util_printf_accepts_limits_and_preserves_formats() {
let o = go(
"event.value = [util.printf('%4096d', 1).length, util.printf('%04096d', -1).length, \
util.printf('%.4096f', 1).length, util.printf('%.4096s', 'a'.repeat(4097)).length, \
util.printf('%4096x', 255).length, util.printf('%.4096x', 255), \
util.printf('%4096d'.repeat(255) + '%4095d', 1).length, util.printf('%4096d'.repeat(256), 1).length, \
util.printf('é'.repeat(524288)).length, util.printf('a%%b:%+06d|%4s|%.2s|%x', 42, 'é', 'é界a', 255)].join('|');",
&Event::field("Calculate", "total", ""),
);
assert_eq!(o.error, None);
assert_eq!(o.value, "4096|4096|4098|4096|4096|FF|1048575|1048576|524288|a%b:+00042| é|é界|FF");
}
fn assert_array_error(script: &str) {
let o = go(script, &Event::field("Calculate", "total", ""));
assert!(o.error.as_deref().is_some_and(|e| e.starts_with("TypeError: array length exceeds 1048576")), "{script}: {:?}", o.error);
assert!(o.requests.is_empty() && o.changed.is_empty(), "{script}");
}
#[test]
fn reset_form_and_field_setters_refuse_huge_arrays() {
for script in [
"this.resetForm(new Array(4294967295))",
"getField('total').value = new Array(4294967295)",
"event.target.value = new Array(4294967295)",
"getField('total').textColor = new Array(4294967295)",
"getField('total').fillColor = new Array(4294967295)",
] {
assert_array_error(script);
}
}
#[test]
fn reset_form_and_field_setters_take_arrays_up_to_the_limit() {
let o = go("this.resetForm(new Array(1 << 20)); getField('total').value = new Array(1 << 20);", &Event::field("Calculate", "total", ""));
assert_eq!(o.error, None);
assert!(matches!(&o.requests[..], [Request::Reset(names)] if names.len() == 1 << 20 && names.iter().all(String::is_empty)));
assert_eq!(o.changed.iter().find(|f| f.name == "total").unwrap().value, [""]);
assert_array_error("this.resetForm(new Array((1 << 20) + 1))");
assert_array_error("getField('total').value = new Array((1 << 20) + 1)");
}
#[test]
fn reset_form_and_field_setters_still_read_short_arrays() {
let mut f = fields();
f.push(FieldState::new("pick", FieldType::ListBox, vec![]));
let o = run(
"this.resetForm(['a', 'b']); getField('pick').value = ['x', 'y', 'z']; getField('total').value = ['x', 'y', 'z'];",
&Event::field("Calculate", "total", ""),
&doc(),
&f,
&[],
Limits::default(),
);
assert_eq!(o.error, None);
assert_eq!(o.requests, [Request::Reset(vec!["a".into(), "b".into()])]);
assert_eq!(o.changed.iter().find(|f| f.name == "pick").unwrap().value, ["x", "y", "z"]);
assert_eq!(o.changed.iter().find(|f| f.name == "total").unwrap().value, ["x"]);
}
#[test]
fn document_requests_and_console() {
let o = go(