mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix(js): refuse code compiled from strings that skipped the script checks
eval and the Function constructors compiled code built at run time, which never passed the size and nesting checks a script passes before it runs, so a short form script could hand boa's recursive parser a deeply nested string and overflow the stack. Form scripts now refuse to compile code from strings. XFA scripts, which the runner itself starts through eval, hold such code to the same checks as the script.
This commit is contained in:
+41
-1
@@ -27,9 +27,11 @@ use std::cell::RefCell;
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use boa_engine::context::HostHooks;
|
||||
use boa_engine::object::builtins::{JsArray, JsFunction};
|
||||
use boa_engine::object::{FunctionObjectBuilder, ObjectInitializer};
|
||||
use boa_engine::property::Attribute;
|
||||
use boa_engine::realm::Realm;
|
||||
use boa_engine::{Context, JsError, JsNativeError, JsObject, JsResult, JsString, JsValue, NativeFunction, Source, js_string};
|
||||
|
||||
/// The kinds of field, as `field.type` names them.
|
||||
@@ -1128,8 +1130,46 @@ pub fn run_shared(script: &str, event: &Event, doc: &DocInfo, fields: Arc<Vec<Fi
|
||||
run_here(script, event, doc, fields, doc_scripts, limits)
|
||||
}
|
||||
|
||||
/// Host hooks that refuse to compile code from strings (`eval`, `Function` and its generator
|
||||
/// and async kin). Such code is built at run time, so it never passed [`refuse`], and boa's
|
||||
/// recursive parser would overflow the stack on a deeply nested one.
|
||||
struct NoCodeFromStrings;
|
||||
|
||||
impl HostHooks for NoCodeFromStrings {
|
||||
fn ensure_can_compile_strings(&self, _: Realm, _: &[JsString], _: &JsString, _: bool, _: &mut Context) -> JsResult<()> {
|
||||
Err(JsNativeError::typ().with_message("scripts can't compile code from strings (eval, Function)").into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Host hooks that hold code compiled from strings to the checks a script passes ([`refuse`]),
|
||||
/// for runners that start the script itself through `eval`.
|
||||
pub(crate) struct CheckedCodeFromStrings;
|
||||
|
||||
impl HostHooks for CheckedCodeFromStrings {
|
||||
fn ensure_can_compile_strings(&self, _: Realm, parameters: &[JsString], body: &JsString, _: bool, _: &mut Context) -> JsResult<()> {
|
||||
let units = parameters.iter().chain(std::iter::once(body)).fold(0usize, |n, s| n.saturating_add(s.len()));
|
||||
if units > MAX_SCRIPT_BYTES {
|
||||
return Err(JsNativeError::range().with_message("the code is too long to compile").into());
|
||||
}
|
||||
// As one text: brackets left open in one parameter carry into the next.
|
||||
let mut source = parameters.iter().map(JsString::to_std_string_lossy).collect::<Vec<_>>().join(",");
|
||||
source.push('\n');
|
||||
source.push_str(&body.to_std_string_lossy());
|
||||
match refuse(&source) {
|
||||
Some(why) => Err(JsNativeError::range().with_message(why).into()),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_here(script: &str, event: &Event, doc: &DocInfo, fields: Arc<Vec<FieldState>>, doc_scripts: &[String], limits: Limits) -> Outcome {
|
||||
let mut ctx = Context::default();
|
||||
let mut ctx = match Context::builder().host_hooks(std::rc::Rc::new(NoCodeFromStrings)).build() {
|
||||
Ok(ctx) => ctx,
|
||||
Err(e) => {
|
||||
let error = Some(format!("the script engine could not start: {e}"));
|
||||
return Outcome { rc: true, value: event.value.clone(), change: event.change.clone(), error, ..Default::default() };
|
||||
}
|
||||
};
|
||||
ctx.runtime_limits_mut().set_loop_iteration_limit(limits.loop_iterations);
|
||||
ctx.runtime_limits_mut().set_recursion_limit(limits.recursion);
|
||||
ctx.insert_data(RefCell::new(Host {
|
||||
|
||||
@@ -313,6 +313,23 @@ fn ordinary_nesting_still_runs() {
|
||||
assert_eq!(go(&s, &Event::field("Calculate", "a", "")).value, "1000");
|
||||
}
|
||||
|
||||
/// Code built from strings at run time would reach the parser without the size and nesting
|
||||
/// checks the script itself passed, so `eval` and the `Function` constructors refuse it.
|
||||
#[test]
|
||||
fn scripts_cannot_compile_code_from_strings() {
|
||||
for s in [
|
||||
"event.value = eval('1 + 1');",
|
||||
"event.value = (0, eval)('1 + 1');",
|
||||
"event.value = new Function('return 2')();",
|
||||
"event.value = Function('a', 'return a')(2);",
|
||||
"event.value = (function* () {}).constructor('yield 2')().next().value;",
|
||||
"event.value = new Array(1001).join('(') + '1' + new Array(1001).join(')'); event.value = eval(event.value);",
|
||||
] {
|
||||
let o = go(s, &Event::field("Calculate", "a", ""));
|
||||
assert!(o.error.as_deref().is_some_and(|e| e.contains("compile code from strings")), "{s}: {o:?}");
|
||||
}
|
||||
}
|
||||
|
||||
mod xfa_model {
|
||||
use crate::Limits;
|
||||
use crate::xfa::*;
|
||||
@@ -398,6 +415,22 @@ mod xfa_model {
|
||||
assert_eq!(o.effects, vec![XfaEffect::SetValue { som: "form1[0].page1[0].total[0]".into(), value: "5".into() }]);
|
||||
}
|
||||
|
||||
/// The script runs through `eval`, so code built from strings at run time is held to the
|
||||
/// script's own size and nesting checks instead of being refused outright.
|
||||
#[test]
|
||||
fn code_built_from_strings_is_checked_like_the_script() {
|
||||
let total = "form1[0].page1[0].total[0]";
|
||||
assert_eq!(go("eval('qty.rawValue * 2')", total, "calculate").result.as_deref(), Some("8"));
|
||||
assert_eq!(go("new Function('return 3')()", total, "calculate").result.as_deref(), Some("3"));
|
||||
for s in [
|
||||
"eval(new Array(1001).join('(') + '1' + new Array(1001).join(')'))",
|
||||
"new Function(new Array(1001).join('[') + new Array(1001).join(']'))",
|
||||
] {
|
||||
let o = go(s, total, "calculate");
|
||||
assert!(o.error.as_deref().is_some_and(|e| e.contains("nested too deeply")), "{s}: {o:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A form whose one field `d` holds the ISO date 2026-10-20, shown as 20261020.
|
||||
pub(super) fn dated_form() -> XfaNode {
|
||||
let d = XfaNode { formatted: Some("20261020".into()), ..field("d", "form1[0].page1[0].d[0]", "2026-10-20", false) };
|
||||
|
||||
@@ -1473,7 +1473,10 @@ pub(crate) fn warm_up() {
|
||||
/// `started` is called once the engine is ready: a time limit on the script runs from then.
|
||||
pub(crate) fn run_here(script: &str, event: &XfaEvent, doc: &XfaDoc, root: XfaNode, limits: Limits, started: &dyn Fn()) -> XfaOutcome {
|
||||
warm_up();
|
||||
let mut ctx = Context::default();
|
||||
let mut ctx = match Context::builder().host_hooks(std::rc::Rc::new(crate::CheckedCodeFromStrings)).build() {
|
||||
Ok(ctx) => ctx,
|
||||
Err(e) => return XfaOutcome { error: Some(format!("the script engine could not start: {e}")), ..Default::default() },
|
||||
};
|
||||
ctx.runtime_limits_mut().set_loop_iteration_limit(limits.loop_iterations);
|
||||
ctx.runtime_limits_mut().set_recursion_limit(limits.recursion);
|
||||
let host_state = XHost::new(root, &event.target, doc);
|
||||
|
||||
Reference in New Issue
Block a user