mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-12 19:55:14 +02:00
279 lines
9.8 KiB
Rust
279 lines
9.8 KiB
Rust
|
|
//! JavaScript per-shape acceptance tests (Phase 13 — Track B JS / TS vertical).
|
||
|
|
//!
|
||
|
|
//! For each [`nyx_scanner::dynamic::lang::js_shared::JsShape`] this suite
|
||
|
|
//! asserts:
|
||
|
|
//!
|
||
|
|
//! 1. The vuln fixture confirms (cmdi / xss oracle fires on the process
|
||
|
|
//! backend, sink probe lights up).
|
||
|
|
//! 2. The benign fixture does NOT confirm.
|
||
|
|
//!
|
||
|
|
//! Framework-bound shapes (Express / Koa / Next.js / browser-event under
|
||
|
|
//! jsdom) skip with an `eprintln!` when the package is unimportable in the
|
||
|
|
//! host's `node` interpreter — `prepare_node`'s `npm install --no-save`
|
||
|
|
//! would otherwise hang on a clean offline CI environment. In a developer
|
||
|
|
//! workstation with the framework installed globally / via the lockfile,
|
||
|
|
//! the test attempts the full pipeline.
|
||
|
|
|
||
|
|
mod common;
|
||
|
|
|
||
|
|
#[cfg(feature = "dynamic")]
|
||
|
|
mod javascript_fixture_tests {
|
||
|
|
use crate::common::fixture_harness::run_shape_fixture_lang;
|
||
|
|
use nyx_scanner::dynamic::spec::PayloadSlot;
|
||
|
|
use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus};
|
||
|
|
use nyx_scanner::labels::Cap;
|
||
|
|
use nyx_scanner::symbol::Lang;
|
||
|
|
|
||
|
|
fn node_available() -> bool {
|
||
|
|
std::process::Command::new("node")
|
||
|
|
.arg("--version")
|
||
|
|
.output()
|
||
|
|
.map(|o| o.status.success())
|
||
|
|
.unwrap_or(false)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn node_module_available(name: &'static str) -> bool {
|
||
|
|
std::process::Command::new("node")
|
||
|
|
.arg("-e")
|
||
|
|
.arg(format!("require.resolve('{name}')"))
|
||
|
|
.output()
|
||
|
|
.map(|o| o.status.success())
|
||
|
|
.unwrap_or(false)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||
|
|
assert_eq!(
|
||
|
|
result.status,
|
||
|
|
VerifyStatus::Confirmed,
|
||
|
|
"{shape}/vuln: expected Confirmed, got {:?} ({:?})",
|
||
|
|
result.status,
|
||
|
|
result.detail,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
fn assert_not_confirmed(shape: &str, result: &VerifyResult) {
|
||
|
|
assert!(
|
||
|
|
matches!(
|
||
|
|
result.status,
|
||
|
|
VerifyStatus::NotConfirmed | VerifyStatus::Inconclusive
|
||
|
|
),
|
||
|
|
"{shape}/benign: expected NotConfirmed (or Inconclusive), got {:?} ({:?})",
|
||
|
|
result.status,
|
||
|
|
result.detail,
|
||
|
|
);
|
||
|
|
assert_ne!(
|
||
|
|
result.status,
|
||
|
|
VerifyStatus::Confirmed,
|
||
|
|
"{shape}/benign: must not confirm",
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
fn run(
|
||
|
|
shape: &str,
|
||
|
|
file: &str,
|
||
|
|
func: &str,
|
||
|
|
cap: Cap,
|
||
|
|
sink_line: u32,
|
||
|
|
kind: EntryKind,
|
||
|
|
slot: PayloadSlot,
|
||
|
|
) -> VerifyResult {
|
||
|
|
run_shape_fixture_lang(
|
||
|
|
Lang::JavaScript,
|
||
|
|
"javascript",
|
||
|
|
shape,
|
||
|
|
file,
|
||
|
|
func,
|
||
|
|
cap,
|
||
|
|
sink_line,
|
||
|
|
kind,
|
||
|
|
slot,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── commonjs_export ─────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn commonjs_export_vuln_is_confirmed() {
|
||
|
|
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||
|
|
let r = run(
|
||
|
|
"commonjs_export", "vuln.js", "runPing", Cap::CODE_EXEC, 11,
|
||
|
|
EntryKind::Function, PayloadSlot::Param(0),
|
||
|
|
);
|
||
|
|
assert_confirmed("commonjs_export", &r);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn commonjs_export_benign_not_confirmed() {
|
||
|
|
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||
|
|
let r = run(
|
||
|
|
"commonjs_export", "benign.js", "runPing", Cap::CODE_EXEC, 11,
|
||
|
|
EntryKind::Function, PayloadSlot::Param(0),
|
||
|
|
);
|
||
|
|
assert_not_confirmed("commonjs_export", &r);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── async_function ──────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn async_function_vuln_is_confirmed() {
|
||
|
|
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||
|
|
let r = run(
|
||
|
|
"async_function", "vuln.js", "runPing", Cap::CODE_EXEC, 15,
|
||
|
|
EntryKind::Function, PayloadSlot::Param(0),
|
||
|
|
);
|
||
|
|
assert_confirmed("async_function", &r);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn async_function_benign_not_confirmed() {
|
||
|
|
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||
|
|
let r = run(
|
||
|
|
"async_function", "benign.js", "runPing", Cap::CODE_EXEC, 14,
|
||
|
|
EntryKind::Function, PayloadSlot::Param(0),
|
||
|
|
);
|
||
|
|
assert_not_confirmed("async_function", &r);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── esm_default ─────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn esm_default_vuln_is_confirmed() {
|
||
|
|
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||
|
|
let r = run(
|
||
|
|
"esm_default", "vuln.js", "runPing", Cap::CODE_EXEC, 14,
|
||
|
|
EntryKind::Function, PayloadSlot::Param(0),
|
||
|
|
);
|
||
|
|
assert_confirmed("esm_default", &r);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn esm_default_benign_not_confirmed() {
|
||
|
|
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||
|
|
let r = run(
|
||
|
|
"esm_default", "benign.js", "runPing", Cap::CODE_EXEC, 14,
|
||
|
|
EntryKind::Function, PayloadSlot::Param(0),
|
||
|
|
);
|
||
|
|
assert_not_confirmed("esm_default", &r);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── express (framework-bound) ───────────────────────────────────────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn express_vuln_is_confirmed() {
|
||
|
|
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||
|
|
if !node_module_available("express") {
|
||
|
|
eprintln!("SKIP: express not importable");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let r = run(
|
||
|
|
"express", "vuln.js", "ping", Cap::CODE_EXEC, 15,
|
||
|
|
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||
|
|
);
|
||
|
|
assert_confirmed("express", &r);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn express_benign_not_confirmed() {
|
||
|
|
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||
|
|
if !node_module_available("express") {
|
||
|
|
eprintln!("SKIP: express not importable");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let r = run(
|
||
|
|
"express", "benign.js", "ping", Cap::CODE_EXEC, 14,
|
||
|
|
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||
|
|
);
|
||
|
|
assert_not_confirmed("express", &r);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── koa (framework-bound) ───────────────────────────────────────────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn koa_vuln_is_confirmed() {
|
||
|
|
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||
|
|
if !node_module_available("koa") {
|
||
|
|
eprintln!("SKIP: koa not importable");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let r = run(
|
||
|
|
"koa", "vuln.js", "ping", Cap::CODE_EXEC, 14,
|
||
|
|
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||
|
|
);
|
||
|
|
assert_confirmed("koa", &r);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn koa_benign_not_confirmed() {
|
||
|
|
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||
|
|
if !node_module_available("koa") {
|
||
|
|
eprintln!("SKIP: koa not importable");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let r = run(
|
||
|
|
"koa", "benign.js", "ping", Cap::CODE_EXEC, 14,
|
||
|
|
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||
|
|
);
|
||
|
|
assert_not_confirmed("koa", &r);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── next_route (framework-bound) ────────────────────────────────────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn next_route_vuln_is_confirmed() {
|
||
|
|
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||
|
|
if !node_module_available("next") {
|
||
|
|
eprintln!("SKIP: next not importable");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let r = run(
|
||
|
|
"next_route", "vuln.js", "handler", Cap::CODE_EXEC, 17,
|
||
|
|
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||
|
|
);
|
||
|
|
assert_confirmed("next_route", &r);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn next_route_benign_not_confirmed() {
|
||
|
|
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||
|
|
if !node_module_available("next") {
|
||
|
|
eprintln!("SKIP: next not importable");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let r = run(
|
||
|
|
"next_route", "benign.js", "handler", Cap::CODE_EXEC, 14,
|
||
|
|
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||
|
|
);
|
||
|
|
assert_not_confirmed("next_route", &r);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── browser_event (jsdom) ───────────────────────────────────────────────
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn browser_event_vuln_is_confirmed() {
|
||
|
|
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||
|
|
if !node_module_available("jsdom") {
|
||
|
|
eprintln!("SKIP: jsdom not importable");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let r = run(
|
||
|
|
"browser_event", "vuln.js", "clickHandler", Cap::HTML_ESCAPE, 14,
|
||
|
|
EntryKind::Function, PayloadSlot::Param(0),
|
||
|
|
);
|
||
|
|
assert_confirmed("browser_event", &r);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn browser_event_benign_not_confirmed() {
|
||
|
|
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||
|
|
if !node_module_available("jsdom") {
|
||
|
|
eprintln!("SKIP: jsdom not importable");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let r = run(
|
||
|
|
"browser_event", "benign.js", "clickHandler", Cap::HTML_ESCAPE, 14,
|
||
|
|
EntryKind::Function, PayloadSlot::Param(0),
|
||
|
|
);
|
||
|
|
assert_not_confirmed("browser_event", &r);
|
||
|
|
}
|
||
|
|
}
|