mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-03 06:52:21 -06:00
Scripting: run-time compiled code passes the nesting check; bounded file masks, socket line search and file reads
eval and the Function constructors compiled script-built strings without the bracket-nesting check the script itself passes, so a short script could overflow the parser's stack. A host hook now checks every dynamic compilation. Folder.getFiles turned masks into a backtracking regular expression; a two-pointer wildcard match replaces it. Socket readln rescanned the whole buffer after every read; only new bytes are searched. Script files that are not regular files (pipes, devices) are not read.
This commit is contained in:
@@ -292,6 +292,22 @@ function Folder(path) {
|
||||
if (!(this instanceof Folder)) return new Folder(path);
|
||||
this.__path = path === undefined ? "" : __str(path);
|
||||
}
|
||||
|
||||
// A case-insensitive `*` / `?` file mask match in time bounded by mask × name length (a
|
||||
// regular expression of `.*` runs backtracks combinatorially on overlapping wildcards).
|
||||
function __globMatch(mask, name) {
|
||||
var p = mask.toLowerCase(), s = name.toLowerCase();
|
||||
var i = 0, j = 0, star = -1, mark = 0;
|
||||
while (j < s.length) {
|
||||
if (i < p.length && (p[i] === "?" || p[i] === s[j])) { i++; j++; }
|
||||
else if (i < p.length && p[i] === "*") { star = i++; mark = j; }
|
||||
else if (star !== -1) { i = star + 1; j = ++mark; }
|
||||
else return false;
|
||||
}
|
||||
while (i < p.length && p[i] === "*") i++;
|
||||
return i === p.length;
|
||||
}
|
||||
|
||||
Folder.prototype = {
|
||||
constructor: Folder,
|
||||
get fsName() { return this.__path; },
|
||||
@@ -310,10 +326,7 @@ Folder.prototype = {
|
||||
var out = [];
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
var n = names[i];
|
||||
if (typeof mask === "string" && mask !== "*") {
|
||||
var re = new RegExp("^" + mask.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$", "i");
|
||||
if (!re.test(n.split(/[\/\\]/).pop())) continue;
|
||||
}
|
||||
if (typeof mask === "string" && mask !== "*" && !__globMatch(mask, n.split(/[\/\\]/).pop())) continue;
|
||||
var isDir = JSON.parse(__file("isDir", n));
|
||||
var f = isDir ? new Folder(n) : new File(n);
|
||||
if (typeof mask === "function" && !mask(f)) continue;
|
||||
|
||||
@@ -512,8 +512,29 @@ fn native_eval_file(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResul
|
||||
r
|
||||
}
|
||||
|
||||
/// Host hooks: code a script compiles at run time (`eval`, `new Function`, …) passes the same
|
||||
/// nesting check as the script itself, since the parser recurses per level and a deep string
|
||||
/// built at run time would overflow the stack.
|
||||
struct Hooks;
|
||||
|
||||
impl boa_engine::context::HostHooks for Hooks {
|
||||
fn ensure_can_compile_strings(
|
||||
&self,
|
||||
_realm: boa_engine::realm::Realm,
|
||||
parameters: &[JsString],
|
||||
body: &JsString,
|
||||
_direct: bool,
|
||||
_context: &mut Context,
|
||||
) -> JsResult<()> {
|
||||
let mut code: String = parameters.iter().map(|p| p.to_std_string_escaped()).collect::<Vec<_>>().join(",");
|
||||
code.push('\n');
|
||||
code.push_str(&body.to_std_string_escaped());
|
||||
check_nesting(&code).map_err(|e| JsNativeError::syntax().with_message(e).into())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_context() -> Result<Context, String> {
|
||||
let mut ctx = Context::default();
|
||||
let mut ctx = boa_engine::context::ContextBuilder::new().host_hooks(std::rc::Rc::new(Hooks)).build().map_err(|e| e.to_string())?;
|
||||
let limits = ctx.runtime_limits_mut();
|
||||
limits.set_loop_iteration_limit(200_000_000);
|
||||
limits.set_recursion_limit(2000);
|
||||
|
||||
@@ -56,6 +56,10 @@ pub(crate) fn read(s: &Session, path: &Path) -> Result<String, String> {
|
||||
if std::fs::metadata(path).is_ok_and(|m| m.len() > MAX_FILE_BYTES as u64) {
|
||||
return Err(too_big());
|
||||
}
|
||||
// A pipe or device reports no size and may never end: only regular files are read.
|
||||
if std::fs::metadata(path).is_ok_and(|m| !m.is_file()) {
|
||||
return Err(format!("Unable to read {}: not a regular file", path.display()));
|
||||
}
|
||||
let bytes = s.services.read_file(&path.to_string_lossy()).map_err(|e| format!("Unable to read {}: {e}", path.display()))?;
|
||||
if bytes.len() > MAX_FILE_BYTES {
|
||||
return Err(too_big());
|
||||
|
||||
@@ -142,7 +142,7 @@ mod imp {
|
||||
|
||||
/// Fill a stream's buffer until `done(buf)` or the timeout / end of stream. Fails only when
|
||||
/// the read deadline can't be set.
|
||||
fn fill(s: &mut TcpStream, buf: &mut Vec<u8>, eof: &mut bool, limit: Duration, done: impl Fn(&[u8]) -> bool) -> std::io::Result<()> {
|
||||
fn fill(s: &mut TcpStream, buf: &mut Vec<u8>, eof: &mut bool, limit: Duration, mut done: impl FnMut(&[u8]) -> bool) -> std::io::Result<()> {
|
||||
let end = Instant::now() + limit;
|
||||
let mut chunk = [0u8; 4096];
|
||||
while !done(buf) && !*eof {
|
||||
@@ -322,7 +322,15 @@ mod imp {
|
||||
}
|
||||
}
|
||||
("readln", Sock::Stream { s, buf, eof }) => {
|
||||
if let Err(e) = fill(s, buf, eof, timeout(p), |b| b.contains(&b'\n')) {
|
||||
// Only the bytes that arrived since the last look are searched: a peer
|
||||
// sending no newline must not make every read rescan the whole buffer.
|
||||
let mut seen = 0;
|
||||
let newline = |b: &[u8]| {
|
||||
let hit = b.get(seen..).is_some_and(|new| new.contains(&b'\n'));
|
||||
seen = b.len();
|
||||
hit
|
||||
};
|
||||
if let Err(e) = fill(s, buf, eof, timeout(p), newline) {
|
||||
return err(format!("cannot set the read timeout: {e}"));
|
||||
}
|
||||
let line: Vec<u8> = match buf.iter().position(|&c| c == b'\n') {
|
||||
|
||||
@@ -1911,3 +1911,27 @@ fn setting_comp_width_and_height_leaves_the_layers_where_they_are() {
|
||||
let (_, c) = comp_named(&s, "w");
|
||||
assert_eq!(layer(c, "s").props.prop("transform/position").unwrap().value, KV::Vec3([100.0, 100.0, 0.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_compiled_at_run_time_passes_the_nesting_check() {
|
||||
// `eval` and `new Function` of a string nested past the limit are refused with a catchable
|
||||
// error instead of overflowing the parser's stack; shallow code still compiles.
|
||||
let mut s = session();
|
||||
for call in ["eval(deep)", "new Function(deep)", "Function('a', deep)"] {
|
||||
let code = format!(
|
||||
"var deep = new Array(5000).join('(') + '1' + new Array(5000).join(')'); var r; try {{ {call}; r = 'ran'; }} catch (e) {{ r = 'refused'; }} r"
|
||||
);
|
||||
assert_eq!(ok(&mut s, &code).result.as_str(), Some("refused"), "{call}");
|
||||
}
|
||||
assert_eq!(ok(&mut s, "eval('1 + (2 * 3)') + new Function('return 4')()").result.as_f64(), Some(11.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_masks_match_without_backtracking() {
|
||||
let mut s = session();
|
||||
let r = ok(
|
||||
&mut s,
|
||||
"[__globMatch('*.JSX', 'a.jsx'), __globMatch('a?c*', 'abcdef'), __globMatch('*a*b', 'xaxb'), __globMatch('*.png', 'a.jsx'), __globMatch('*a*a*a*a*a*a*a*a*a*a*a*b', new Array(60).join('a'))].join()",
|
||||
);
|
||||
assert_eq!(r.result.as_str(), Some("true,true,true,false,false"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user