mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-29 12:38:58 -06:00
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:
co-authored by
Claude Sonnet 5.5
parent
265ddca58c
commit
1b6e144c57
+24
-2
@@ -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 (`!`, `~`, `+`, `-`).
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user