Merge pull request #561 from mnaza/fix/string-timer-global-scope-558
fix(js): run string timer handlers as global-scope scripts at fire time
This commit is contained in:
10
crates/obscura-js/js/bootstrap.js
vendored
10
crates/obscura-js/js/bootstrap.js
vendored
@@ -481,7 +481,15 @@ const _scheduleAfter = (delay, fn) => {
|
|||||||
// call silently no-ops and JS-triggered navigations (cookie → reload) never fire.
|
// call silently no-ops and JS-triggered navigations (cookie → reload) never fire.
|
||||||
const _coerceTimerFn = (fn) => {
|
const _coerceTimerFn = (fn) => {
|
||||||
if (typeof fn === "string") {
|
if (typeof fn === "string") {
|
||||||
try { return new Function(fn); } catch (_) { return null; }
|
// Per HTML, a string handler is compiled and run as a classic script in
|
||||||
|
// global scope *at fire time*. Indirect eval ((0, eval)) runs in the true
|
||||||
|
// global scope, so top-level var/function declarations become globals (a
|
||||||
|
// `new Function(fn)` wrapper kept them local); deferring to fire time also
|
||||||
|
// surfaces a SyntaxError when the timer elapses, matching a real browser,
|
||||||
|
// instead of swallowing it eagerly at scheduling. The dynamic-script path
|
||||||
|
// uses the same indirect eval for the same reason.
|
||||||
|
const src = fn;
|
||||||
|
return () => { (0, eval)(src); };
|
||||||
}
|
}
|
||||||
return typeof fn === "function" ? fn : null;
|
return typeof fn === "function" ? fn : null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1330,6 +1330,26 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn string_timeout_declarations_reach_global_scope() {
|
||||||
|
// A string timer handler runs as a classic script in global scope, so a
|
||||||
|
// top-level var/function declaration in it becomes a global. new Function()
|
||||||
|
// kept those declarations local to the compiled function, so they never
|
||||||
|
// reached globalThis.
|
||||||
|
let mut rt = setup_runtime("<html><body></body></html>");
|
||||||
|
rt.evaluate(
|
||||||
|
"setTimeout('var __leaked = 42; function __leakedFn(){ return 7; }', 0)",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
rt.run_event_loop_bounded(100).await.unwrap();
|
||||||
|
let v = rt
|
||||||
|
.evaluate(
|
||||||
|
"String(globalThis.__leaked) + '|' + (typeof globalThis.__leakedFn === 'function' ? globalThis.__leakedFn() : 'missing')",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(v, serde_json::json!("42|7"));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn string_interval_handler_repeats_and_can_clear_itself() {
|
async fn string_interval_handler_repeats_and_can_clear_itself() {
|
||||||
let mut rt = setup_runtime("<html><body></body></html>");
|
let mut rt = setup_runtime("<html><body></body></html>");
|
||||||
|
|||||||
Reference in New Issue
Block a user