use std::sync::Arc;
use jsdet_core::{CompiledModule, EmptyBridge, Observation, SandboxConfig};
fn module() -> CompiledModule {
CompiledModule::new().unwrap()
}
fn config() -> SandboxConfig {
SandboxConfig {
timeout_ms: 5000,
max_fuel: 100_000_000,
..SandboxConfig::default()
}
}
#[test]
fn infinite_loop_is_bounded() {
let result = module().execute(&["while(true) {}".into()], Arc::new(EmptyBridge), &config());
assert!(result.is_ok() || result.is_err());
}
#[test]
fn stack_overflow_is_caught() {
let result = module().execute(
&["function f() { f(); } f();".into()],
Arc::new(EmptyBridge),
&config(),
);
match result {
Ok(r) => assert!(!r.errors.is_empty() || !r.observations.is_empty()),
Err(_) => {} }
}
#[test]
fn memory_bomb_is_bounded() {
let result = module().execute(
&["var a = 'x'; while(true) { a = a + a; }".into()],
Arc::new(EmptyBridge),
&config(),
);
assert!(result.is_ok() || result.is_err());
}
#[test]
fn prototype_pollution_does_not_crash() {
let result = module().execute(
&[r#"
Object.prototype.__proto__ = null;
({}).__proto__.__proto__ = {};
Object.defineProperty(Object.prototype, 'polluted', {value: 'yes'});
var x = {};
if (x.polluted !== 'yes') throw 'pollution failed';
"#
.into()],
Arc::new(EmptyBridge),
&config(),
);
match result {
Ok(r) => assert_eq!(r.scripts_executed, 1),
Err(_) => {} }
}
#[test]
fn eval_chain_does_not_escape() {
let result = module().execute(
&[r#"
eval("eval('eval(\"1+1\")')");
"#
.into()],
Arc::new(EmptyBridge),
&config(),
);
match result {
Ok(r) => assert_eq!(r.scripts_executed, 1),
Err(_) => {}
}
}
#[test]
fn regex_dos_is_bounded() {
let result = module().execute(
&[r#"
var re = /^(a+)+$/;
re.test("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaab");
"#
.into()],
Arc::new(EmptyBridge),
&config(),
);
assert!(result.is_ok() || result.is_err());
}
#[test]
fn constructor_abuse_does_not_escape() {
let result = module().execute(
&[r#"
var F = Function;
var f = new F("return this");
var global = f();
"#
.into()],
Arc::new(EmptyBridge),
&config(),
);
match result {
Ok(r) => assert_eq!(r.scripts_executed, 1),
Err(_) => {}
}
}
#[test]
fn typed_array_oob_does_not_crash() {
let result = module().execute(
&[r#"
var buf = new ArrayBuffer(8);
var view = new Uint8Array(buf);
try { view[1000000] = 42; } catch(e) {}
try { view[-1] = 42; } catch(e) {}
"#
.into()],
Arc::new(EmptyBridge),
&config(),
);
match result {
Ok(r) => assert_eq!(r.scripts_executed, 1),
Err(_) => {}
}
}
#[test]
fn symbol_species_abuse_does_not_crash() {
let result = module().execute(
&[r#"
class Evil extends Array {
static get [Symbol.species]() { return Array; }
}
var e = new Evil(1, 2, 3);
var mapped = e.map(x => x * 2);
"#
.into()],
Arc::new(EmptyBridge),
&config(),
);
match result {
Ok(r) => assert_eq!(r.scripts_executed, 1),
Err(_) => {}
}
}
#[test]
fn proxy_trap_bomb_does_not_crash() {
let result = module().execute(
&[r#"
var handler = {
get: function(target, name) {
return new Proxy({}, handler);
}
};
var p = new Proxy({}, handler);
try { p.a.b.c.d.e.f.g.h.i.j; } catch(e) {}
"#
.into()],
Arc::new(EmptyBridge),
&config(),
);
assert!(result.is_ok() || result.is_err());
}
#[test]
fn isolation_between_runs_is_absolute() {
let m = module();
let bridge = Arc::new(EmptyBridge);
let cfg = config();
let _ = m.execute(
&[r#"
globalThis.SECRET = "password123";
Object.prototype.leaked = true;
"#
.into()],
bridge.clone(),
&cfg,
);
let r2 = m
.execute(
&[r#"
if (typeof globalThis.SECRET !== 'undefined') throw 'GLOBAL_LEAK';
if (({}).leaked === true) throw 'PROTO_LEAK';
"#
.into()],
bridge,
&cfg,
)
.unwrap();
let has_leak = r2
.observations
.iter()
.any(|o| matches!(o, Observation::Error { message, .. } if message.contains("LEAK")));
assert!(!has_leak, "state must not leak between executions");
}
#[test]
fn cannot_detect_via_error_stack() {
let r = module()
.execute(
&[r#"
try {
null.x;
} catch(e) {
var stack = e.stack || '';
if (stack.indexOf('wasm') >= 0 || stack.indexOf('jsdet') >= 0) {
throw new Error('SANDBOX_DETECTED_VIA_STACK');
}
}
"#
.into()],
Arc::new(EmptyBridge),
&config(),
)
.unwrap();
let detected = r.observations.iter().any(
|o| matches!(o, Observation::Error { message, .. } if message.contains("SANDBOX_DETECTED")),
);
if detected {
eprintln!("WARNING: sandbox detectable via error stacks (known gap)");
}
}
#[test]
fn cannot_detect_via_function_tostring() {
let r = module()
.execute(
&[r#"
var s = Function.prototype.toString.call(eval);
// Real browser: "function eval() { [native code] }"
// If it shows JS source, it's a hook
"#
.into()],
Arc::new(EmptyBridge),
&config(),
)
.unwrap();
assert!(
r.errors.is_empty(),
"Function.toString must not crash: {:?}",
r.errors
);
}
#[test]
fn promise_rejection_does_not_crash() {
let r = module()
.execute(
&[r#"
Promise.reject(new Error('unhandled'));
Promise.all([Promise.reject('fail1'), Promise.reject('fail2')]);
new Promise(function(_, reject) { reject('boom'); });
"#
.into()],
Arc::new(EmptyBridge),
&config(),
)
.unwrap();
assert_eq!(r.scripts_executed, 1, "unhandled rejections must not crash");
}
#[test]
fn recursive_json_stringify_bounded() {
let r = module().execute(
&[r#"
var a = {};
a.self = a;
try { JSON.stringify(a); } catch(e) { /* expected */ }
"#
.into()],
Arc::new(EmptyBridge),
&config(),
);
assert!(r.is_ok());
}
#[test]
fn regexp_redos_bounded_by_fuel() {
let r = module().execute(
&[r#"
var evil = 'a'.repeat(30) + 'b';
/^(a+)+$/.test(evil);
"#
.into()],
Arc::new(EmptyBridge),
&SandboxConfig {
timeout_ms: 5000,
max_fuel: 50_000_000,
..SandboxConfig::default()
},
);
assert!(r.is_ok() || r.is_err());
}
#[test]
fn symbol_to_primitive_does_not_crash() {
let r = module()
.execute(
&[r#"
var obj = {};
obj[Symbol.toPrimitive] = function(hint) {
if (hint === 'number') return 42;
return 'forty-two';
};
'' + obj; // triggers toPrimitive
+obj; // triggers numeric conversion
"#
.into()],
Arc::new(EmptyBridge),
&config(),
)
.unwrap();
assert_eq!(r.scripts_executed, 1);
}
#[test]
fn proxy_trap_does_not_escape() {
let r = module()
.execute(
&[r#"
var handler = {
get: function(target, prop) {
if (prop === 'then') return undefined; // thenable trap
return Reflect.get(target, prop);
},
set: function(target, prop, value) {
return Reflect.set(target, prop, value);
},
has: function(target, prop) {
return true; // lie about having everything
}
};
var p = new Proxy({}, handler);
p.x = 1;
if (!('anything' in p)) throw 'proxy broken';
"#
.into()],
Arc::new(EmptyBridge),
&config(),
)
.unwrap();
assert_eq!(r.scripts_executed, 1);
}