jsdet-core 0.1.0

Core WASM-sandboxed JavaScript detonation engine
Documentation
/// Taint tracking tests.
///
/// These tests verify that taint labels propagate through string operations
/// at the QuickJS engine level. The taint is set by the host (via bridge calls)
/// and checked when tainted data reaches a sink.
///
/// Note: Full taint testing requires the bridge to set taint on incoming values
/// and check taint at sink calls. These tests verify the C-level propagation
/// works by running JS string operations and checking that observations include
/// taint information.
use std::sync::Arc;

use jsdet_core::{CompiledModule, EmptyBridge, SandboxConfig};

#[test]
fn string_concat_preserves_execution() {
    // Verify that string operations work correctly in the WASM sandbox.
    // Taint propagation is C-level — these tests confirm the patched QuickJS
    // handles string operations without crashing.
    let module = CompiledModule::new().unwrap();
    let result = module
        .execute(
            &[r#"
                var a = "hello";
                var b = " world";
                var c = a + b;
                if (c !== "hello world") throw "concat failed: " + c;
            "#
            .into()],
            Arc::new(EmptyBridge),
            &SandboxConfig::default(),
        )
        .unwrap();
    assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
}

#[test]
fn string_slice_preserves_execution() {
    let module = CompiledModule::new().unwrap();
    let result = module
        .execute(
            &[r#"
                var s = "abcdefgh";
                var sliced = s.slice(2, 5);
                if (sliced !== "cde") throw "slice failed: " + sliced;
            "#
            .into()],
            Arc::new(EmptyBridge),
            &SandboxConfig::default(),
        )
        .unwrap();
    assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
}

#[test]
fn string_replace_preserves_execution() {
    let module = CompiledModule::new().unwrap();
    let result = module
        .execute(
            &[r#"
                var s = "hello world";
                var replaced = s.replace("world", "jsdet");
                if (replaced !== "hello jsdet") throw "replace failed: " + replaced;
            "#
            .into()],
            Arc::new(EmptyBridge),
            &SandboxConfig::default(),
        )
        .unwrap();
    assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
}

#[test]
fn string_split_preserves_execution() {
    let module = CompiledModule::new().unwrap();
    let result = module
        .execute(
            &[r#"
                var s = "a,b,c";
                var parts = s.split(",");
                if (parts.length !== 3) throw "split count failed: " + parts.length;
                if (parts[1] !== "b") throw "split value failed: " + parts[1];
            "#
            .into()],
            Arc::new(EmptyBridge),
            &SandboxConfig::default(),
        )
        .unwrap();
    assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
}

#[test]
fn template_literal_preserves_execution() {
    let module = CompiledModule::new().unwrap();
    let result = module
        .execute(
            &[r#"
                var name = "jsdet";
                var greeting = `hello ${name}!`;
                if (greeting !== "hello jsdet!") throw "template failed: " + greeting;
            "#
            .into()],
            Arc::new(EmptyBridge),
            &SandboxConfig::default(),
        )
        .unwrap();
    assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
}

#[test]
fn json_parse_preserves_execution() {
    let module = CompiledModule::new().unwrap();
    let result = module
        .execute(
            &[r#"
                var json = '{"name":"jsdet","version":1}';
                var obj = JSON.parse(json);
                if (obj.name !== "jsdet") throw "json parse failed: " + obj.name;
                if (obj.version !== 1) throw "json version failed: " + obj.version;
            "#
            .into()],
            Arc::new(EmptyBridge),
            &SandboxConfig::default(),
        )
        .unwrap();
    assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
}

#[test]
fn complex_string_chain_preserves_execution() {
    let module = CompiledModule::new().unwrap();
    let result = module
        .execute(
            &[r#"
                // Chain: JSON parse → substring → concat → replace
                // This is the exact data flow in phishing kits.
                var payload = '{"url":"https://evil.com/exfil","token":"abc123"}';
                var obj = JSON.parse(payload);
                var path = obj.url.substring(obj.url.indexOf("/", 8));
                var full = "https://legit.com" + path;
                var encoded = full.replace("exfil", "data");
                if (!encoded.includes("legit.com")) throw "chain failed: " + encoded;
            "#
            .into()],
            Arc::new(EmptyBridge),
            &SandboxConfig::default(),
        )
        .unwrap();
    assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
}

#[test]
fn taint_set_and_get_roundtrip() {
    let module = CompiledModule::new().unwrap();
    let result = module
        .execute(
            &[r#"
            var s = "tainted_data";
            __jsdet_set_taint(s, 7);
            var label = __jsdet_get_taint(s);
            if (label !== 7) throw "taint roundtrip failed: got " + label;
        "#
            .into()],
            Arc::new(EmptyBridge),
            &SandboxConfig::default(),
        )
        .unwrap();
    assert!(
        result.errors.is_empty(),
        "taint roundtrip should succeed: {:?}",
        result.errors
    );
}

#[test]
fn taint_get_returns_zero_for_clean_string() {
    let module = CompiledModule::new().unwrap();
    let result = module
        .execute(
            &[r#"
            var clean = "no taint";
            var label = __jsdet_get_taint(clean);
            if (label !== 0) throw "clean string should have label 0, got " + label;
        "#
            .into()],
            Arc::new(EmptyBridge),
            &SandboxConfig::default(),
        )
        .unwrap();
    assert!(result.errors.is_empty(), "{:?}", result.errors);
}

#[test]
fn taint_get_returns_zero_for_non_string() {
    let module = CompiledModule::new().unwrap();
    let result = module
        .execute(
            &[r#"
            var num = 42;
            var label = __jsdet_get_taint(num);
            if (label !== 0) throw "non-string should have label 0, got " + label;
        "#
            .into()],
            Arc::new(EmptyBridge),
            &SandboxConfig::default(),
        )
        .unwrap();
    assert!(result.errors.is_empty(), "{:?}", result.errors);
}

#[test]
fn taint_propagates_through_plus_operator() {
    let module = CompiledModule::new().unwrap();
    let result = module
        .execute(
            &[r#"
            var t = "payload";
            __jsdet_set_taint(t, 1);
            var result = "prefix_" + t;
            var label = __jsdet_get_taint(result);
            if (label === 0) throw "concat result should be tainted, got label " + label;
        "#
            .into()],
            Arc::new(EmptyBridge),
            &SandboxConfig::default(),
        )
        .unwrap();
    assert!(
        result.errors.is_empty(),
        "taint should propagate through +: {:?}",
        result.errors
    );
}

#[test]
fn taint_propagates_through_substring() {
    let module = CompiledModule::new().unwrap();
    let result = module
        .execute(
            &[r#"
            var t = "abcPAYLOADxyz";
            __jsdet_set_taint(t, 1);
            var sub = t.substring(3, 10);
            var label = __jsdet_get_taint(sub);
            if (label === 0) throw "substring should preserve taint, got " + label;
        "#
            .into()],
            Arc::new(EmptyBridge),
            &SandboxConfig::default(),
        )
        .unwrap();
    assert!(result.errors.is_empty(), "{:?}", result.errors);
}

#[test]
fn taint_check_at_sink_detects_tainted_input() {
    let module = CompiledModule::new().unwrap();
    let result = module
        .execute(
            &[r#"
            var t = "evil_code";
            __jsdet_set_taint(t, 1);
            var detected = __jsdet_check_taint_at_sink("eval", t);
            if (!detected) throw "should detect tainted input at sink";
        "#
            .into()],
            Arc::new(EmptyBridge),
            &SandboxConfig::default(),
        )
        .unwrap();
    assert!(result.errors.is_empty(), "{:?}", result.errors);
}

#[test]
fn taint_check_at_sink_ignores_clean_input() {
    let module = CompiledModule::new().unwrap();
    let result = module
        .execute(
            &[r#"
            var clean = "safe_code";
            var detected = __jsdet_check_taint_at_sink("eval", clean);
            if (detected) throw "should NOT detect clean input at sink";
        "#
            .into()],
            Arc::new(EmptyBridge),
            &SandboxConfig::default(),
        )
        .unwrap();
    assert!(result.errors.is_empty(), "{:?}", result.errors);
}

#[test]
fn taint_survives_json_parse_stringify() {
    let module = CompiledModule::new().unwrap();
    let result = module
        .execute(
            &[r#"
            var obj = {key: "value"};
            var json = JSON.stringify(obj);
            __jsdet_set_taint(json, 1);
            var parsed = JSON.parse(json);
            // After JSON.parse, the string values should carry taint
            // (depends on QuickJS JSON.parse implementation)
        "#
            .into()],
            Arc::new(EmptyBridge),
            &SandboxConfig::default(),
        )
        .unwrap();
    // Just verify it doesn't crash — JSON parse taint propagation is best-effort
    assert!(result.errors.is_empty(), "{:?}", result.errors);
}

#[test]
fn taint_multiple_labels_on_different_strings() {
    let module = CompiledModule::new().unwrap();
    let result = module
        .execute(
            &[r#"
            var s1 = "source_1";
            var s2 = "source_2";
            __jsdet_set_taint(s1, 1);
            __jsdet_set_taint(s2, 2);
            if (__jsdet_get_taint(s1) !== 1) throw "s1 should have label 1";
            if (__jsdet_get_taint(s2) !== 2) throw "s2 should have label 2";
        "#
            .into()],
            Arc::new(EmptyBridge),
            &SandboxConfig::default(),
        )
        .unwrap();
    assert!(result.errors.is_empty(), "{:?}", result.errors);
}

#[test]
fn taint_overwrite_label() {
    let module = CompiledModule::new().unwrap();
    let result = module
        .execute(
            &[r#"
            var s = "data";
            __jsdet_set_taint(s, 5);
            if (__jsdet_get_taint(s) !== 5) throw "should be 5";
            __jsdet_set_taint(s, 10);
            if (__jsdet_get_taint(s) !== 10) throw "should be 10 after overwrite";
        "#
            .into()],
            Arc::new(EmptyBridge),
            &SandboxConfig::default(),
        )
        .unwrap();
    assert!(result.errors.is_empty(), "{:?}", result.errors);
}