mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-21 20:18:06 +02:00
[pitboss] phase 13: Track B — JavaScript + TypeScript harness emitter shapes
This commit is contained in:
parent
96eb37500c
commit
34a5879459
51 changed files with 2556 additions and 440 deletions
|
|
@ -192,22 +192,10 @@ fn stage_fixture(src: &Path, tmp: &TempDir, copy: CopyStrategy) -> PathBuf {
|
|||
}
|
||||
}
|
||||
|
||||
/// Phase 12 — per-shape acceptance helper.
|
||||
/// Phase 12 — Python-specific per-shape acceptance helper.
|
||||
///
|
||||
/// Stages `fixture_root/<shape>/<file>` into a tempdir, builds a
|
||||
/// [`HarnessSpec`] with the caller's `entry_kind` / `payload_slot`,
|
||||
/// then executes it through [`nyx_scanner::dynamic::runner::run_spec`]
|
||||
/// directly. Returns a [`VerifyResult`]-shaped summary so callers can
|
||||
/// reuse the same `assert_confirmed` / `assert_not_confirmed` helpers
|
||||
/// the older golden-based suite uses.
|
||||
///
|
||||
/// Bypasses [`verify_finding`] because the public verifier derives the
|
||||
/// payload slot from the synthetic Diag's flow steps and always lands
|
||||
/// on [`nyx_scanner::dynamic::spec::PayloadSlot::Param`], which the
|
||||
/// HTTP / pytest / CLI shapes cannot honour. Going through the runner
|
||||
/// directly lets the test pin the slot the spec under test actually
|
||||
/// expects (e.g. [`nyx_scanner::dynamic::spec::PayloadSlot::QueryParam`]
|
||||
/// for HTTP routes).
|
||||
/// Thin wrapper over [`run_shape_fixture_lang`] pinning the lang dir
|
||||
/// to `tests/dynamic_fixtures/python/` and [`Lang::Python`].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn run_shape_fixture(
|
||||
shape_dir: &str,
|
||||
|
|
@ -217,16 +205,54 @@ pub fn run_shape_fixture(
|
|||
sink_line: u32,
|
||||
entry_kind: EntryKind,
|
||||
payload_slot: nyx_scanner::dynamic::spec::PayloadSlot,
|
||||
) -> VerifyResult {
|
||||
run_shape_fixture_lang(
|
||||
nyx_scanner::symbol::Lang::Python,
|
||||
"python",
|
||||
shape_dir,
|
||||
file,
|
||||
func,
|
||||
cap,
|
||||
sink_line,
|
||||
entry_kind,
|
||||
payload_slot,
|
||||
)
|
||||
}
|
||||
|
||||
/// Phase 13 — lang-aware per-shape acceptance helper.
|
||||
///
|
||||
/// Stages `tests/dynamic_fixtures/<lang_dir>/<shape>/<file>` into a
|
||||
/// tempdir, builds a [`HarnessSpec`] with the caller's `entry_kind` /
|
||||
/// `payload_slot` / [`Lang`], then executes it through
|
||||
/// [`nyx_scanner::dynamic::runner::run_spec`] directly. Returns a
|
||||
/// [`VerifyResult`]-shaped summary so callers can reuse the same
|
||||
/// `assert_confirmed` / `assert_not_confirmed` helpers across Python /
|
||||
/// JS / TS / etc. shape suites.
|
||||
///
|
||||
/// Bypasses [`verify_finding`] for the same reason as [`run_shape_fixture`]:
|
||||
/// the public verifier always lands on
|
||||
/// [`nyx_scanner::dynamic::spec::PayloadSlot::Param`].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn run_shape_fixture_lang(
|
||||
lang: nyx_scanner::symbol::Lang,
|
||||
lang_dir: &str,
|
||||
shape_dir: &str,
|
||||
file: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
entry_kind: EntryKind,
|
||||
payload_slot: nyx_scanner::dynamic::spec::PayloadSlot,
|
||||
) -> VerifyResult {
|
||||
use nyx_scanner::dynamic::runner::{run_spec, RunError};
|
||||
use nyx_scanner::dynamic::sandbox::SandboxOptions;
|
||||
use nyx_scanner::dynamic::spec::{HarnessSpec, SpecDerivationStrategy};
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
||||
let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
let fixture_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/dynamic_fixtures/python")
|
||||
.join("tests/dynamic_fixtures")
|
||||
.join(lang_dir)
|
||||
.join(shape_dir);
|
||||
let fixture_src = fixture_root.join(file);
|
||||
|
||||
|
|
@ -245,8 +271,10 @@ pub fn run_shape_fixture(
|
|||
|
||||
let entry_file = dst.to_string_lossy().into_owned();
|
||||
// Per-fixture stable hash so workdir layout / cache key stays
|
||||
// distinct between shapes and between vuln / benign fixtures.
|
||||
// distinct between langs / shapes / vuln-vs-benign fixtures.
|
||||
let mut digest = blake3::Hasher::new();
|
||||
digest.update(lang_dir.as_bytes());
|
||||
digest.update(b"|");
|
||||
digest.update(shape_dir.as_bytes());
|
||||
digest.update(b"|");
|
||||
digest.update(file.as_bytes());
|
||||
|
|
@ -255,13 +283,25 @@ pub fn run_shape_fixture(
|
|||
u64::from_le_bytes(bytes.as_bytes()[..8].try_into().unwrap())
|
||||
});
|
||||
|
||||
let toolchain_id = match lang {
|
||||
nyx_scanner::symbol::Lang::Python => "python-3",
|
||||
nyx_scanner::symbol::Lang::JavaScript | nyx_scanner::symbol::Lang::TypeScript => "node-20",
|
||||
nyx_scanner::symbol::Lang::Rust => "rust-stable",
|
||||
nyx_scanner::symbol::Lang::Go => "go-1.21",
|
||||
nyx_scanner::symbol::Lang::Java => "java-17",
|
||||
nyx_scanner::symbol::Lang::Php => "php-8",
|
||||
nyx_scanner::symbol::Lang::Ruby => "ruby-3",
|
||||
nyx_scanner::symbol::Lang::C => "gcc",
|
||||
nyx_scanner::symbol::Lang::Cpp => "g++",
|
||||
};
|
||||
|
||||
let spec = HarnessSpec {
|
||||
finding_id: spec_hash.clone(),
|
||||
entry_file: entry_file.clone(),
|
||||
entry_name: func.to_owned(),
|
||||
entry_kind,
|
||||
lang: Lang::Python,
|
||||
toolchain_id: "python-3".into(),
|
||||
lang,
|
||||
toolchain_id: toolchain_id.into(),
|
||||
payload_slot,
|
||||
expected_cap: cap,
|
||||
constraint_hints: vec![],
|
||||
|
|
@ -332,15 +372,10 @@ pub fn run_shape_fixture(
|
|||
}
|
||||
}
|
||||
|
||||
/// Phase 12 — golden harness snapshot.
|
||||
/// Phase 12 — Python-specific harness snapshot wrapper.
|
||||
///
|
||||
/// Stages `<shape>/<file>` into a tempdir, builds a [`HarnessSpec`] for
|
||||
/// the supplied entry kind / payload slot, emits the per-shape harness
|
||||
/// via [`nyx_scanner::dynamic::lang::emit`], and either writes the
|
||||
/// resulting source to `<shape>/<file>.golden_harness.py` (under
|
||||
/// `NYX_UPDATE_GOLDENS=1`) or diffs against the existing snapshot. The
|
||||
/// emitter is deterministic, so the snapshot doubles as documentation
|
||||
/// of the per-shape harness shape.
|
||||
/// Pins lang to [`Lang::Python`] and the lang dir to `python` so legacy
|
||||
/// Python tests can keep their original two-axis signature.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn run_harness_snapshot(
|
||||
shape_dir: &str,
|
||||
|
|
@ -351,17 +386,52 @@ pub fn run_harness_snapshot(
|
|||
entry_kind: EntryKind,
|
||||
payload_slot: nyx_scanner::dynamic::spec::PayloadSlot,
|
||||
) {
|
||||
use nyx_scanner::dynamic::lang;
|
||||
run_harness_snapshot_lang(
|
||||
nyx_scanner::symbol::Lang::Python,
|
||||
"python",
|
||||
"py",
|
||||
shape_dir,
|
||||
file,
|
||||
func,
|
||||
cap,
|
||||
sink_line,
|
||||
entry_kind,
|
||||
payload_slot,
|
||||
)
|
||||
}
|
||||
|
||||
/// Phase 13 — lang-aware golden harness snapshot.
|
||||
///
|
||||
/// Stages `tests/dynamic_fixtures/<lang_dir>/<shape>/<file>` into a
|
||||
/// tempdir, builds a [`HarnessSpec`] for the supplied lang / entry kind
|
||||
/// / payload slot, emits the per-shape harness via
|
||||
/// [`nyx_scanner::dynamic::lang::emit`], and either writes the resulting
|
||||
/// source to `<shape>/<file>.golden_harness.<ext>` (under
|
||||
/// `NYX_UPDATE_GOLDENS=1`) or diffs against the existing snapshot.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn run_harness_snapshot_lang(
|
||||
lang: nyx_scanner::symbol::Lang,
|
||||
lang_dir: &str,
|
||||
snapshot_ext: &str,
|
||||
shape_dir: &str,
|
||||
file: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
entry_kind: EntryKind,
|
||||
payload_slot: nyx_scanner::dynamic::spec::PayloadSlot,
|
||||
) {
|
||||
use nyx_scanner::dynamic::lang as lang_emit;
|
||||
use nyx_scanner::dynamic::spec::{HarnessSpec, SpecDerivationStrategy};
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
||||
let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
let fixture_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/dynamic_fixtures/python")
|
||||
.join("tests/dynamic_fixtures")
|
||||
.join(lang_dir)
|
||||
.join(shape_dir);
|
||||
let fixture_src = fixture_root.join(file);
|
||||
let snapshot_path = fixture_root.join(format!("{file}.golden_harness.py"));
|
||||
let snapshot_path = fixture_root.join(format!("{file}.golden_harness.{snapshot_ext}"));
|
||||
|
||||
// Stage into tempdir so the spec.entry_file path matches what the
|
||||
// verifier sees at runtime.
|
||||
|
|
@ -370,13 +440,19 @@ pub fn run_harness_snapshot(
|
|||
std::fs::copy(&fixture_src, &dst).expect("copy fixture into tempdir");
|
||||
let entry_file = dst.to_string_lossy().into_owned();
|
||||
|
||||
let toolchain_id = match lang {
|
||||
nyx_scanner::symbol::Lang::Python => "python-3",
|
||||
nyx_scanner::symbol::Lang::JavaScript | nyx_scanner::symbol::Lang::TypeScript => "node-20",
|
||||
_ => "unknown",
|
||||
};
|
||||
|
||||
let spec = HarnessSpec {
|
||||
finding_id: "0000000000000001".into(),
|
||||
entry_file: entry_file.clone(),
|
||||
entry_name: func.to_owned(),
|
||||
entry_kind,
|
||||
lang: Lang::Python,
|
||||
toolchain_id: "python-3".into(),
|
||||
lang,
|
||||
toolchain_id: toolchain_id.into(),
|
||||
payload_slot,
|
||||
expected_cap: cap,
|
||||
constraint_hints: vec![],
|
||||
|
|
@ -389,7 +465,7 @@ pub fn run_harness_snapshot(
|
|||
stubs_required: vec![],
|
||||
};
|
||||
|
||||
let harness = lang::emit(&spec).expect("python emitter must produce a harness");
|
||||
let harness = lang_emit::emit(&spec).expect("emitter must produce a harness");
|
||||
|
||||
// Strip the tempdir prefix so the snapshot is stable across runs.
|
||||
let tmp_prefix = tmp.path().to_string_lossy().into_owned();
|
||||
|
|
|
|||
24
tests/dynamic_fixtures/javascript/async_function/benign.js
Normal file
24
tests/dynamic_fixtures/javascript/async_function/benign.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Phase 13 — bare async function, benign control.
|
||||
//
|
||||
// execFile (no shell) via util.promisify(execFile). Payload never reaches a
|
||||
// shell; stderr silenced so payload bytes do not leak via the inner process'
|
||||
// error message.
|
||||
|
||||
'use strict';
|
||||
const { execFile } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execFileP = promisify(execFile);
|
||||
|
||||
async function runPing(host) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
const { stdout } = await execFileP('true', [host], {
|
||||
timeout: 5000,
|
||||
});
|
||||
return stdout;
|
||||
} catch (_e) {
|
||||
return 'err';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { runPing };
|
||||
25
tests/dynamic_fixtures/javascript/async_function/vuln.js
Normal file
25
tests/dynamic_fixtures/javascript/async_function/vuln.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Phase 13 — bare async function, vulnerable.
|
||||
//
|
||||
// Stdlib-only. Async function awaits `child_process.exec` via util.promisify
|
||||
// so the harness's `await _entry.runPing(payload)` resolves before the
|
||||
// process exits.
|
||||
|
||||
'use strict';
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execP = promisify(exec);
|
||||
|
||||
async function runPing(host) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
const { stdout } = await execP('echo hello ' + host, { timeout: 5000 });
|
||||
process.stdout.write(stdout);
|
||||
return stdout;
|
||||
} catch (e) {
|
||||
const out = (e.stdout || '') + (e.stderr || '');
|
||||
process.stdout.write(out);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { runPing };
|
||||
19
tests/dynamic_fixtures/javascript/browser_event/benign.js
Normal file
19
tests/dynamic_fixtures/javascript/browser_event/benign.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Phase 13 — browser-side event handler, benign control.
|
||||
//
|
||||
// Uses `textContent` so the payload's `<script>` tag is HTML-escaped before
|
||||
// serialisation; the XSS oracle marker cannot appear in stdout because
|
||||
// `<` becomes `<`.
|
||||
|
||||
'use strict';
|
||||
// nyx-shape: browser-event
|
||||
|
||||
function clickHandler(payload) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
const el = document.getElementById('out');
|
||||
if (el) {
|
||||
el.textContent = String(payload);
|
||||
}
|
||||
return el ? el.textContent : '';
|
||||
}
|
||||
|
||||
module.exports = { clickHandler };
|
||||
12
tests/dynamic_fixtures/javascript/browser_event/package-lock.json
generated
Normal file
12
tests/dynamic_fixtures/javascript/browser_event/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "nyx-harness-jsdom",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nyx-harness-jsdom",
|
||||
"version": "0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "nyx-harness-jsdom",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"jsdom": "^24.1.1"
|
||||
}
|
||||
}
|
||||
21
tests/dynamic_fixtures/javascript/browser_event/vuln.js
Normal file
21
tests/dynamic_fixtures/javascript/browser_event/vuln.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Phase 13 — browser-side event handler, vulnerable.
|
||||
//
|
||||
// Harness spins up jsdom (js_shared::emit_browser_event), assigns
|
||||
// `globalThis.document`, then calls `clickHandler(payload)`. The handler
|
||||
// writes payload into innerHTML — the XSS oracle's `<script>NYX_XSS_CONFIRMED
|
||||
// </script>` payload appears in the serialised DOM the harness mirrors to
|
||||
// stdout.
|
||||
|
||||
'use strict';
|
||||
// nyx-shape: browser-event
|
||||
|
||||
function clickHandler(payload) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
const el = document.getElementById('out');
|
||||
if (el) {
|
||||
el.innerHTML = String(payload);
|
||||
}
|
||||
return el ? el.innerHTML : '';
|
||||
}
|
||||
|
||||
module.exports = { clickHandler };
|
||||
20
tests/dynamic_fixtures/javascript/commonjs_export/benign.js
Normal file
20
tests/dynamic_fixtures/javascript/commonjs_export/benign.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Phase 13 — CommonJS export, benign control.
|
||||
|
||||
'use strict';
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
function runPing(host) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
execFileSync('true', [host], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
return 'ok';
|
||||
} catch (_e) {
|
||||
return 'err';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { runPing };
|
||||
21
tests/dynamic_fixtures/javascript/commonjs_export/vuln.js
Normal file
21
tests/dynamic_fixtures/javascript/commonjs_export/vuln.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Phase 13 — CommonJS export, vulnerable.
|
||||
//
|
||||
// Synchronous `execSync` with shell:true via string concat. Stdlib only.
|
||||
|
||||
'use strict';
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
function runPing(host) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
const out = execSync('echo hello ' + host, { encoding: 'utf8', timeout: 5000 });
|
||||
process.stdout.write(out);
|
||||
return out;
|
||||
} catch (e) {
|
||||
const out = (e.stdout || '') + (e.stderr || '');
|
||||
process.stdout.write(out);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { runPing };
|
||||
18
tests/dynamic_fixtures/javascript/esm_default/benign.js
Normal file
18
tests/dynamic_fixtures/javascript/esm_default/benign.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Phase 13 — ES module default export, benign control.
|
||||
//
|
||||
// nyx-shape: esm-default
|
||||
import { execFileSync } from 'child_process';
|
||||
|
||||
export default function runPing(host) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
execFileSync('true', [host], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
return 'ok';
|
||||
} catch (_e) {
|
||||
return 'err';
|
||||
}
|
||||
}
|
||||
22
tests/dynamic_fixtures/javascript/esm_default/vuln.js
Normal file
22
tests/dynamic_fixtures/javascript/esm_default/vuln.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// Phase 13 — ES module default export, vulnerable.
|
||||
//
|
||||
// `export default` body is the entry the harness imports dynamically. The
|
||||
// harness builder stages this file at `workdir/entry.mjs` (per
|
||||
// js_shared::entry_subpath_for_shape) so Node parses it under ESM semantics
|
||||
// regardless of the on-disk `.js` extension under the fixture tree.
|
||||
|
||||
// nyx-shape: esm-default
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
export default function runPing(host) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
const out = execSync('echo hello ' + host, { encoding: 'utf8', timeout: 5000 });
|
||||
process.stdout.write(out);
|
||||
return out;
|
||||
} catch (e) {
|
||||
const out = (e.stdout || '') + (e.stderr || '');
|
||||
process.stdout.write(out);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
28
tests/dynamic_fixtures/javascript/express/benign.js
Normal file
28
tests/dynamic_fixtures/javascript/express/benign.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Phase 13 — Express route handler, benign control.
|
||||
//
|
||||
// Uses execFile (no shell) so the payload bytes are never interpreted as
|
||||
// shell metacharacters. The oracle marker cannot appear in stdout because
|
||||
// the inner child reads `true` and its stdio is ignored.
|
||||
|
||||
'use strict';
|
||||
const express = require('express');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
function ping(req, res) {
|
||||
const host = (req.query && req.query.host) || '';
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
execFileSync('true', [host], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
res.send('ok');
|
||||
} catch (_e) {
|
||||
res.send('err');
|
||||
}
|
||||
}
|
||||
|
||||
void express;
|
||||
|
||||
module.exports = { ping };
|
||||
12
tests/dynamic_fixtures/javascript/express/package-lock.json
generated
Normal file
12
tests/dynamic_fixtures/javascript/express/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "nyx-harness-express",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nyx-harness-express",
|
||||
"version": "0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
tests/dynamic_fixtures/javascript/express/package.json
Normal file
8
tests/dynamic_fixtures/javascript/express/package.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "nyx-harness-express",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"express": "^4.19.2"
|
||||
}
|
||||
}
|
||||
26
tests/dynamic_fixtures/javascript/express/vuln.js
Normal file
26
tests/dynamic_fixtures/javascript/express/vuln.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Phase 13 — Express route handler, vulnerable.
|
||||
//
|
||||
// Vulnerable handler concatenates `req.query.host` into a shell command.
|
||||
// Harness builds a mock req/res via js_shared::emit_express and dispatches
|
||||
// synchronously; we never bind a real listener.
|
||||
|
||||
'use strict';
|
||||
const express = require('express');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
function ping(req, res) {
|
||||
const host = (req.query && req.query.host) || '';
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
const out = execSync('echo hello ' + host, { encoding: 'utf8', timeout: 5000 });
|
||||
res.send(out);
|
||||
} catch (e) {
|
||||
res.send((e.stdout || '') + (e.stderr || ''));
|
||||
}
|
||||
}
|
||||
|
||||
// Touch the dep so the materialised package.json's `express` pin survives
|
||||
// shake-down by `npm install --no-save`; harness never starts the server.
|
||||
void express;
|
||||
|
||||
module.exports = { ping };
|
||||
26
tests/dynamic_fixtures/javascript/koa/benign.js
Normal file
26
tests/dynamic_fixtures/javascript/koa/benign.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Phase 13 — Koa middleware, benign control.
|
||||
//
|
||||
// execFile (no shell), stderr silenced, child writes nothing to stdout.
|
||||
|
||||
'use strict';
|
||||
const Koa = require('koa');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
async function ping(ctx) {
|
||||
const host = (ctx.query && ctx.query.host) || '';
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
execFileSync('true', [host], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
ctx.body = 'ok';
|
||||
} catch (_e) {
|
||||
ctx.body = 'err';
|
||||
}
|
||||
}
|
||||
|
||||
void Koa;
|
||||
|
||||
module.exports = { ping };
|
||||
12
tests/dynamic_fixtures/javascript/koa/package-lock.json
generated
Normal file
12
tests/dynamic_fixtures/javascript/koa/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "nyx-harness-koa",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nyx-harness-koa",
|
||||
"version": "0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
tests/dynamic_fixtures/javascript/koa/package.json
Normal file
8
tests/dynamic_fixtures/javascript/koa/package.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "nyx-harness-koa",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"koa": "^2.15.3"
|
||||
}
|
||||
}
|
||||
23
tests/dynamic_fixtures/javascript/koa/vuln.js
Normal file
23
tests/dynamic_fixtures/javascript/koa/vuln.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Phase 13 — Koa middleware, vulnerable.
|
||||
//
|
||||
// Vulnerable middleware reads `ctx.query.host` and concatenates it into a
|
||||
// shell command. Harness builds a mock ctx via js_shared::emit_koa.
|
||||
|
||||
'use strict';
|
||||
const Koa = require('koa');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
async function ping(ctx) {
|
||||
const host = (ctx.query && ctx.query.host) || '';
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
const out = execSync('echo hello ' + host, { encoding: 'utf8', timeout: 5000 });
|
||||
ctx.body = out;
|
||||
} catch (e) {
|
||||
ctx.body = (e.stdout || '') + (e.stderr || '');
|
||||
}
|
||||
}
|
||||
|
||||
void Koa;
|
||||
|
||||
module.exports = { ping };
|
||||
25
tests/dynamic_fixtures/javascript/next_route/benign.js
Normal file
25
tests/dynamic_fixtures/javascript/next_route/benign.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Phase 13 — Next.js API route handler, benign control.
|
||||
//
|
||||
// execFile (no shell) so payload bytes never reach a shell.
|
||||
//
|
||||
// nyx-shape: next
|
||||
|
||||
'use strict';
|
||||
try { require.resolve('next'); } catch (_e) {}
|
||||
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
module.exports = async function handler(req, res) {
|
||||
const host = (req.query && req.query.host) || '';
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
execFileSync('true', [host], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
res.status(200).send('ok');
|
||||
} catch (_e) {
|
||||
res.status(200).send('err');
|
||||
}
|
||||
};
|
||||
12
tests/dynamic_fixtures/javascript/next_route/package-lock.json
generated
Normal file
12
tests/dynamic_fixtures/javascript/next_route/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "nyx-harness-next",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nyx-harness-next",
|
||||
"version": "0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "nyx-harness-next",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"next": "^14.2.5"
|
||||
}
|
||||
}
|
||||
26
tests/dynamic_fixtures/javascript/next_route/vuln.js
Normal file
26
tests/dynamic_fixtures/javascript/next_route/vuln.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Phase 13 — Next.js API route handler, vulnerable.
|
||||
//
|
||||
// Reads `req.query.host` and concatenates it into a shell command. The
|
||||
// `next` package is required for the materialised package.json pin to
|
||||
// survive `npm install --no-save`, but the harness builds its own mock
|
||||
// req/res via js_shared::emit_next; we never go through the Next router.
|
||||
//
|
||||
// nyx-shape: next
|
||||
|
||||
'use strict';
|
||||
// Touching `next` would also load React; the import is intentionally lazy
|
||||
// and guarded so test runs without a network-fed install still parse.
|
||||
try { require.resolve('next'); } catch (_e) {}
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
module.exports = async function handler(req, res) {
|
||||
const host = (req.query && req.query.host) || '';
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
const out = execSync('echo hello ' + host, { encoding: 'utf8', timeout: 5000 });
|
||||
res.status(200).send(out);
|
||||
} catch (e) {
|
||||
res.status(200).send((e.stdout || '') + (e.stderr || ''));
|
||||
}
|
||||
};
|
||||
24
tests/dynamic_fixtures/typescript/async_function/benign.ts
Normal file
24
tests/dynamic_fixtures/typescript/async_function/benign.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Phase 13 — bare async function, benign control.
|
||||
//
|
||||
// execFile (no shell) via util.promisify(execFile). Payload never reaches a
|
||||
// shell; stderr silenced so payload bytes do not leak via the inner process'
|
||||
// error message.
|
||||
|
||||
'use strict';
|
||||
const { execFile } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execFileP = promisify(execFile);
|
||||
|
||||
async function runPing(host) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
const { stdout } = await execFileP('true', [host], {
|
||||
timeout: 5000,
|
||||
});
|
||||
return stdout;
|
||||
} catch (_e) {
|
||||
return 'err';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { runPing };
|
||||
25
tests/dynamic_fixtures/typescript/async_function/vuln.ts
Normal file
25
tests/dynamic_fixtures/typescript/async_function/vuln.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Phase 13 — bare async function, vulnerable.
|
||||
//
|
||||
// Stdlib-only. Async function awaits `child_process.exec` via util.promisify
|
||||
// so the harness's `await _entry.runPing(payload)` resolves before the
|
||||
// process exits.
|
||||
|
||||
'use strict';
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execP = promisify(exec);
|
||||
|
||||
async function runPing(host) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
const { stdout } = await execP('echo hello ' + host, { timeout: 5000 });
|
||||
process.stdout.write(stdout);
|
||||
return stdout;
|
||||
} catch (e) {
|
||||
const out = (e.stdout || '') + (e.stderr || '');
|
||||
process.stdout.write(out);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { runPing };
|
||||
19
tests/dynamic_fixtures/typescript/browser_event/benign.ts
Normal file
19
tests/dynamic_fixtures/typescript/browser_event/benign.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Phase 13 — browser-side event handler, benign control.
|
||||
//
|
||||
// Uses `textContent` so the payload's `<script>` tag is HTML-escaped before
|
||||
// serialisation; the XSS oracle marker cannot appear in stdout because
|
||||
// `<` becomes `<`.
|
||||
|
||||
'use strict';
|
||||
// nyx-shape: browser-event
|
||||
|
||||
function clickHandler(payload) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
const el = document.getElementById('out');
|
||||
if (el) {
|
||||
el.textContent = String(payload);
|
||||
}
|
||||
return el ? el.textContent : '';
|
||||
}
|
||||
|
||||
module.exports = { clickHandler };
|
||||
12
tests/dynamic_fixtures/typescript/browser_event/package-lock.json
generated
Normal file
12
tests/dynamic_fixtures/typescript/browser_event/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "nyx-harness-jsdom",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nyx-harness-jsdom",
|
||||
"version": "0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "nyx-harness-jsdom",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"jsdom": "^24.1.1"
|
||||
}
|
||||
}
|
||||
21
tests/dynamic_fixtures/typescript/browser_event/vuln.ts
Normal file
21
tests/dynamic_fixtures/typescript/browser_event/vuln.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Phase 13 — browser-side event handler, vulnerable.
|
||||
//
|
||||
// Harness spins up jsdom (js_shared::emit_browser_event), assigns
|
||||
// `globalThis.document`, then calls `clickHandler(payload)`. The handler
|
||||
// writes payload into innerHTML — the XSS oracle's `<script>NYX_XSS_CONFIRMED
|
||||
// </script>` payload appears in the serialised DOM the harness mirrors to
|
||||
// stdout.
|
||||
|
||||
'use strict';
|
||||
// nyx-shape: browser-event
|
||||
|
||||
function clickHandler(payload) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
const el = document.getElementById('out');
|
||||
if (el) {
|
||||
el.innerHTML = String(payload);
|
||||
}
|
||||
return el ? el.innerHTML : '';
|
||||
}
|
||||
|
||||
module.exports = { clickHandler };
|
||||
20
tests/dynamic_fixtures/typescript/commonjs_export/benign.ts
Normal file
20
tests/dynamic_fixtures/typescript/commonjs_export/benign.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Phase 13 — CommonJS export, benign control.
|
||||
|
||||
'use strict';
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
function runPing(host) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
execFileSync('true', [host], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
return 'ok';
|
||||
} catch (_e) {
|
||||
return 'err';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { runPing };
|
||||
21
tests/dynamic_fixtures/typescript/commonjs_export/vuln.ts
Normal file
21
tests/dynamic_fixtures/typescript/commonjs_export/vuln.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Phase 13 — CommonJS export, vulnerable.
|
||||
//
|
||||
// Synchronous `execSync` with shell:true via string concat. Stdlib only.
|
||||
|
||||
'use strict';
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
function runPing(host) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
const out = execSync('echo hello ' + host, { encoding: 'utf8', timeout: 5000 });
|
||||
process.stdout.write(out);
|
||||
return out;
|
||||
} catch (e) {
|
||||
const out = (e.stdout || '') + (e.stderr || '');
|
||||
process.stdout.write(out);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { runPing };
|
||||
18
tests/dynamic_fixtures/typescript/esm_default/benign.ts
Normal file
18
tests/dynamic_fixtures/typescript/esm_default/benign.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Phase 13 — ES module default export, benign control.
|
||||
//
|
||||
// nyx-shape: esm-default
|
||||
import { execFileSync } from 'child_process';
|
||||
|
||||
export default function runPing(host) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
execFileSync('true', [host], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
return 'ok';
|
||||
} catch (_e) {
|
||||
return 'err';
|
||||
}
|
||||
}
|
||||
22
tests/dynamic_fixtures/typescript/esm_default/vuln.ts
Normal file
22
tests/dynamic_fixtures/typescript/esm_default/vuln.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// Phase 13 — ES module default export, vulnerable.
|
||||
//
|
||||
// `export default` body is the entry the harness imports dynamically. The
|
||||
// harness builder stages this file at `workdir/entry.mjs` (per
|
||||
// js_shared::entry_subpath_for_shape) so Node parses it under ESM semantics
|
||||
// regardless of the on-disk `.js` extension under the fixture tree.
|
||||
|
||||
// nyx-shape: esm-default
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
export default function runPing(host) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
const out = execSync('echo hello ' + host, { encoding: 'utf8', timeout: 5000 });
|
||||
process.stdout.write(out);
|
||||
return out;
|
||||
} catch (e) {
|
||||
const out = (e.stdout || '') + (e.stderr || '');
|
||||
process.stdout.write(out);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
28
tests/dynamic_fixtures/typescript/express/benign.ts
Normal file
28
tests/dynamic_fixtures/typescript/express/benign.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Phase 13 — Express route handler, benign control.
|
||||
//
|
||||
// Uses execFile (no shell) so the payload bytes are never interpreted as
|
||||
// shell metacharacters. The oracle marker cannot appear in stdout because
|
||||
// the inner child reads `true` and its stdio is ignored.
|
||||
|
||||
'use strict';
|
||||
const express = require('express');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
function ping(req, res) {
|
||||
const host = (req.query && req.query.host) || '';
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
execFileSync('true', [host], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
res.send('ok');
|
||||
} catch (_e) {
|
||||
res.send('err');
|
||||
}
|
||||
}
|
||||
|
||||
void express;
|
||||
|
||||
module.exports = { ping };
|
||||
12
tests/dynamic_fixtures/typescript/express/package-lock.json
generated
Normal file
12
tests/dynamic_fixtures/typescript/express/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "nyx-harness-express",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nyx-harness-express",
|
||||
"version": "0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
tests/dynamic_fixtures/typescript/express/package.json
Normal file
8
tests/dynamic_fixtures/typescript/express/package.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "nyx-harness-express",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"express": "^4.19.2"
|
||||
}
|
||||
}
|
||||
26
tests/dynamic_fixtures/typescript/express/vuln.ts
Normal file
26
tests/dynamic_fixtures/typescript/express/vuln.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Phase 13 — Express route handler, vulnerable.
|
||||
//
|
||||
// Vulnerable handler concatenates `req.query.host` into a shell command.
|
||||
// Harness builds a mock req/res via js_shared::emit_express and dispatches
|
||||
// synchronously; we never bind a real listener.
|
||||
|
||||
'use strict';
|
||||
const express = require('express');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
function ping(req, res) {
|
||||
const host = (req.query && req.query.host) || '';
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
const out = execSync('echo hello ' + host, { encoding: 'utf8', timeout: 5000 });
|
||||
res.send(out);
|
||||
} catch (e) {
|
||||
res.send((e.stdout || '') + (e.stderr || ''));
|
||||
}
|
||||
}
|
||||
|
||||
// Touch the dep so the materialised package.json's `express` pin survives
|
||||
// shake-down by `npm install --no-save`; harness never starts the server.
|
||||
void express;
|
||||
|
||||
module.exports = { ping };
|
||||
26
tests/dynamic_fixtures/typescript/koa/benign.ts
Normal file
26
tests/dynamic_fixtures/typescript/koa/benign.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Phase 13 — Koa middleware, benign control.
|
||||
//
|
||||
// execFile (no shell), stderr silenced, child writes nothing to stdout.
|
||||
|
||||
'use strict';
|
||||
const Koa = require('koa');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
async function ping(ctx) {
|
||||
const host = (ctx.query && ctx.query.host) || '';
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
execFileSync('true', [host], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
ctx.body = 'ok';
|
||||
} catch (_e) {
|
||||
ctx.body = 'err';
|
||||
}
|
||||
}
|
||||
|
||||
void Koa;
|
||||
|
||||
module.exports = { ping };
|
||||
12
tests/dynamic_fixtures/typescript/koa/package-lock.json
generated
Normal file
12
tests/dynamic_fixtures/typescript/koa/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "nyx-harness-koa",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nyx-harness-koa",
|
||||
"version": "0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
tests/dynamic_fixtures/typescript/koa/package.json
Normal file
8
tests/dynamic_fixtures/typescript/koa/package.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "nyx-harness-koa",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"koa": "^2.15.3"
|
||||
}
|
||||
}
|
||||
23
tests/dynamic_fixtures/typescript/koa/vuln.ts
Normal file
23
tests/dynamic_fixtures/typescript/koa/vuln.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Phase 13 — Koa middleware, vulnerable.
|
||||
//
|
||||
// Vulnerable middleware reads `ctx.query.host` and concatenates it into a
|
||||
// shell command. Harness builds a mock ctx via js_shared::emit_koa.
|
||||
|
||||
'use strict';
|
||||
const Koa = require('koa');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
async function ping(ctx) {
|
||||
const host = (ctx.query && ctx.query.host) || '';
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
const out = execSync('echo hello ' + host, { encoding: 'utf8', timeout: 5000 });
|
||||
ctx.body = out;
|
||||
} catch (e) {
|
||||
ctx.body = (e.stdout || '') + (e.stderr || '');
|
||||
}
|
||||
}
|
||||
|
||||
void Koa;
|
||||
|
||||
module.exports = { ping };
|
||||
25
tests/dynamic_fixtures/typescript/next_route/benign.ts
Normal file
25
tests/dynamic_fixtures/typescript/next_route/benign.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Phase 13 — Next.js API route handler, benign control.
|
||||
//
|
||||
// execFile (no shell) so payload bytes never reach a shell.
|
||||
//
|
||||
// nyx-shape: next
|
||||
|
||||
'use strict';
|
||||
try { require.resolve('next'); } catch (_e) {}
|
||||
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
module.exports = async function handler(req, res) {
|
||||
const host = (req.query && req.query.host) || '';
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
execFileSync('true', [host], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
res.status(200).send('ok');
|
||||
} catch (_e) {
|
||||
res.status(200).send('err');
|
||||
}
|
||||
};
|
||||
12
tests/dynamic_fixtures/typescript/next_route/package-lock.json
generated
Normal file
12
tests/dynamic_fixtures/typescript/next_route/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "nyx-harness-next",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nyx-harness-next",
|
||||
"version": "0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "nyx-harness-next",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"next": "^14.2.5"
|
||||
}
|
||||
}
|
||||
26
tests/dynamic_fixtures/typescript/next_route/vuln.ts
Normal file
26
tests/dynamic_fixtures/typescript/next_route/vuln.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Phase 13 — Next.js API route handler, vulnerable.
|
||||
//
|
||||
// Reads `req.query.host` and concatenates it into a shell command. The
|
||||
// `next` package is required for the materialised package.json pin to
|
||||
// survive `npm install --no-save`, but the harness builds its own mock
|
||||
// req/res via js_shared::emit_next; we never go through the Next router.
|
||||
//
|
||||
// nyx-shape: next
|
||||
|
||||
'use strict';
|
||||
// Touching `next` would also load React; the import is intentionally lazy
|
||||
// and guarded so test runs without a network-fed install still parse.
|
||||
try { require.resolve('next'); } catch (_e) {}
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
module.exports = async function handler(req, res) {
|
||||
const host = (req.query && req.query.host) || '';
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
const out = execSync('echo hello ' + host, { encoding: 'utf8', timeout: 5000 });
|
||||
res.status(200).send(out);
|
||||
} catch (e) {
|
||||
res.status(200).send((e.stdout || '') + (e.stderr || ''));
|
||||
}
|
||||
};
|
||||
278
tests/javascript_fixtures.rs
Normal file
278
tests/javascript_fixtures.rs
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
//! 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);
|
||||
}
|
||||
}
|
||||
270
tests/typescript_fixtures.rs
Normal file
270
tests/typescript_fixtures.rs
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
//! TypeScript per-shape acceptance tests (Phase 13 — Track B JS / TS vertical).
|
||||
//!
|
||||
//! Mirrors `tests/javascript_fixtures.rs` against
|
||||
//! `tests/dynamic_fixtures/typescript/<shape>/`. TS fixtures use
|
||||
//! ES-compatible syntax so the harness builder can stage them at
|
||||
//! `workdir/entry.js` and run them through Node's CommonJS / ESM loader
|
||||
//! without a separate `tsc` step.
|
||||
|
||||
mod common;
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod typescript_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::TypeScript,
|
||||
"typescript",
|
||||
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.ts", "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.ts", "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.ts", "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.ts", "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.ts", "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.ts", "runPing", Cap::CODE_EXEC, 14,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("esm_default", &r);
|
||||
}
|
||||
|
||||
// ── express ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[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.ts", "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.ts", "ping", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||||
);
|
||||
assert_not_confirmed("express", &r);
|
||||
}
|
||||
|
||||
// ── koa ─────────────────────────────────────────────────────────────────
|
||||
|
||||
#[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.ts", "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.ts", "ping", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||||
);
|
||||
assert_not_confirmed("koa", &r);
|
||||
}
|
||||
|
||||
// ── next_route ──────────────────────────────────────────────────────────
|
||||
|
||||
#[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.ts", "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.ts", "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.ts", "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.ts", "clickHandler", Cap::HTML_ESCAPE, 14,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("browser_event", &r);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue