mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0017 (20260516T052512Z-20f8)
This commit is contained in:
parent
608929194d
commit
1062846a07
6 changed files with 366 additions and 186 deletions
|
|
@ -279,6 +279,32 @@ def __nyx_install_crash_guard(sink_callee)
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Phase 10 (Track D.3) HTTP recording helper. When the verifier spawned an
|
||||
# HttpStub it publishes the side-channel log path through NYX_HTTP_LOG; a
|
||||
# sink call site whose outbound request never reaches the on-the-wire
|
||||
# listener (DNS-mocked, network-isolated sandbox, pre-flight check) can
|
||||
# call this helper to surface the attempted call. Format matches the
|
||||
# Python / Node / PHP / Go siblings so the host-side HttpStub log-line
|
||||
# merger parses all five streams identically. No-op when NYX_HTTP_LOG is
|
||||
# unset so the same harness still runs cleanly under modes that did not
|
||||
# spawn a stub. Single-quoted Ruby string literals keep this helper free
|
||||
# of the literal hash-after-double-quote sequence that would terminate
|
||||
# the surrounding Rust raw string.
|
||||
def __nyx_stub_http_record(method, url, body = nil, **detail)
|
||||
p = ENV['NYX_HTTP_LOG']
|
||||
return if p.nil? || p.empty?
|
||||
begin
|
||||
File.open(p, 'a') do |f|
|
||||
f.puts('# method: ' + method.to_s)
|
||||
f.puts('# url: ' + url.to_s)
|
||||
f.puts('# body: ' + body.to_s) unless body.nil?
|
||||
detail.each { |k, v| f.puts('# ' + k.to_s + ': ' + v.to_s) }
|
||||
f.puts(method.to_s + ' ' + url.to_s)
|
||||
end
|
||||
rescue StandardError
|
||||
end
|
||||
end
|
||||
"#
|
||||
}
|
||||
|
||||
|
|
@ -778,6 +804,27 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_shim_publishes_stub_http_recorder() {
|
||||
let shim = probe_shim();
|
||||
assert!(
|
||||
shim.contains("def __nyx_stub_http_record"),
|
||||
"Ruby probe shim must define __nyx_stub_http_record"
|
||||
);
|
||||
assert!(
|
||||
shim.contains("ENV['NYX_HTTP_LOG']"),
|
||||
"Ruby HTTP recorder must read NYX_HTTP_LOG to find the side-channel log"
|
||||
);
|
||||
assert!(
|
||||
shim.contains("# method: "),
|
||||
"Ruby HTTP recorder must emit a hash-prefixed method detail line"
|
||||
);
|
||||
assert!(
|
||||
shim.contains("# url: "),
|
||||
"Ruby HTTP recorder must emit a hash-prefixed url detail line"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chain_step_splices_probe_shim_for_composite_reverify() {
|
||||
let step = chain_step(Some(b"<prev>"));
|
||||
|
|
|
|||
|
|
@ -68,6 +68,12 @@ pub enum Prerequisite {
|
|||
/// A static C library archive (e.g. `libc.a`) must be linkable.
|
||||
/// Used by the Phase-17/20 hardening probe fixtures.
|
||||
StaticLib(&'static str),
|
||||
/// A Node.js module must be importable via `require.resolve`. Used
|
||||
/// by the JavaScript / TypeScript framework-bound shape suites
|
||||
/// (express / koa / next / jsdom) so a host without the package on
|
||||
/// the resolution path skips with a structured reason instead of
|
||||
/// failing the test.
|
||||
NodeModuleAvailable(&'static str),
|
||||
}
|
||||
|
||||
/// Phase 29 (Track I): why the harness skipped a fixture. Carried by
|
||||
|
|
@ -80,6 +86,7 @@ pub enum SkipReason {
|
|||
MissingEnvVar(&'static str),
|
||||
DockerUnavailable,
|
||||
MissingStaticLib(&'static str),
|
||||
MissingNodeModule(&'static str),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SkipReason {
|
||||
|
|
@ -89,6 +96,9 @@ impl std::fmt::Display for SkipReason {
|
|||
SkipReason::MissingEnvVar(v) => write!(f, "env var not set: {v}"),
|
||||
SkipReason::DockerUnavailable => write!(f, "docker daemon unavailable"),
|
||||
SkipReason::MissingStaticLib(l) => write!(f, "static lib not linkable: {l}"),
|
||||
SkipReason::MissingNodeModule(m) => {
|
||||
write!(f, "Node module not resolvable via require.resolve: {m}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -125,6 +135,18 @@ pub fn check_prerequisites(reqs: &[Prerequisite]) -> Result<(), SkipReason> {
|
|||
return Err(SkipReason::DockerUnavailable);
|
||||
}
|
||||
}
|
||||
Prerequisite::NodeModuleAvailable(name) => {
|
||||
let probe = format!("require.resolve('{name}')");
|
||||
let ok = std::process::Command::new("node")
|
||||
.arg("-e")
|
||||
.arg(&probe)
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
if !ok {
|
||||
return Err(SkipReason::MissingNodeModule(name));
|
||||
}
|
||||
}
|
||||
Prerequisite::StaticLib(lib) => {
|
||||
// Treat the lib as linkable iff `cc -static -l<lib>` on
|
||||
// an empty TU succeeds. Slow but reliable; only called
|
||||
|
|
|
|||
27
tests/dynamic_fixtures/stubs_e2e/ruby/http/vuln/main.rb
Normal file
27
tests/dynamic_fixtures/stubs_e2e/ruby/http/vuln/main.rb
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Phase 10 (Track D.3) stub-end-to-end fixture: Ruby + HTTP.
|
||||
#
|
||||
# The verifier publishes:
|
||||
#
|
||||
# * NYX_HTTP_ENDPOINT — http://127.0.0.1:{port} the HttpStub listens on.
|
||||
# * NYX_HTTP_LOG — companion log path the harness appends attempted
|
||||
# outbound calls to so the host HttpStub picks them up on
|
||||
# drain_events() even when the request bypasses the on-the-wire
|
||||
# listener (DNS-mocked, network-isolated sandbox, pre-flight check).
|
||||
#
|
||||
# This fixture exercises the side-channel path: it records an attempted
|
||||
# SSRF call to http://169.254.169.254/latest/meta-data/ through the
|
||||
# Ruby shim helper __nyx_stub_http_record without issuing the actual
|
||||
# network call. The companion test in tests/stubs_e2e_per_lang.rs
|
||||
# splices in nyx_scanner::dynamic::lang::ruby::probe_shim ahead of this
|
||||
# source, runs it with both env vars set, and asserts the stub captured
|
||||
# the attempt.
|
||||
|
||||
method = 'GET'
|
||||
url = 'http://169.254.169.254/latest/meta-data/'
|
||||
body = ''
|
||||
# Record the attempted call through the probe shim so the host
|
||||
# HttpStub captures it on the next drain_events() call even when the
|
||||
# harness never reaches the on-the-wire listener.
|
||||
__nyx_stub_http_record(method, url, body, driver: 'net/http')
|
||||
# Echo so the host can confirm the driver ran end-to-end.
|
||||
$stdout.puts(ENV['NYX_HTTP_ENDPOINT'] || 'no-endpoint')
|
||||
|
|
@ -18,28 +18,18 @@ mod common;
|
|||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod javascript_fixture_tests {
|
||||
use crate::common::fixture_harness::run_shape_fixture_lang;
|
||||
use crate::common::fixture_harness::{run_shape_fixture_lang_or_skip, Prerequisite};
|
||||
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)
|
||||
}
|
||||
/// Base prereq slice shared by every JS shape: the host must have
|
||||
/// `node` on PATH. Framework-bound shapes extend the slice with a
|
||||
/// second `Prerequisite::NodeModuleAvailable("<pkg>")` entry so a
|
||||
/// host without the package on the resolution path skips with a
|
||||
/// structured reason rather than failing the test.
|
||||
const NODE_REQ: &[Prerequisite] = &[Prerequisite::CommandAvailable("node")];
|
||||
|
||||
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert_eq!(
|
||||
|
|
@ -68,7 +58,9 @@ mod javascript_fixture_tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn run(
|
||||
requires: &[Prerequisite],
|
||||
shape: &str,
|
||||
file: &str,
|
||||
func: &str,
|
||||
|
|
@ -76,8 +68,9 @@ mod javascript_fixture_tests {
|
|||
sink_line: u32,
|
||||
kind: EntryKind,
|
||||
slot: PayloadSlot,
|
||||
) -> VerifyResult {
|
||||
run_shape_fixture_lang(
|
||||
) -> Option<VerifyResult> {
|
||||
run_shape_fixture_lang_or_skip(
|
||||
requires,
|
||||
Lang::JavaScript,
|
||||
"javascript",
|
||||
shape,
|
||||
|
|
@ -94,21 +87,21 @@ mod javascript_fixture_tests {
|
|||
|
||||
#[test]
|
||||
fn commonjs_export_vuln_is_confirmed() {
|
||||
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
NODE_REQ,
|
||||
"commonjs_export", "vuln.js", "runPing", Cap::CODE_EXEC, 11,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else { return; };
|
||||
assert_confirmed("commonjs_export", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commonjs_export_benign_not_confirmed() {
|
||||
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
NODE_REQ,
|
||||
"commonjs_export", "benign.js", "runPing", Cap::CODE_EXEC, 11,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else { return; };
|
||||
assert_not_confirmed("commonjs_export", &r);
|
||||
}
|
||||
|
||||
|
|
@ -116,21 +109,21 @@ mod javascript_fixture_tests {
|
|||
|
||||
#[test]
|
||||
fn async_function_vuln_is_confirmed() {
|
||||
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
NODE_REQ,
|
||||
"async_function", "vuln.js", "runPing", Cap::CODE_EXEC, 15,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else { return; };
|
||||
assert_confirmed("async_function", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_function_benign_not_confirmed() {
|
||||
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
NODE_REQ,
|
||||
"async_function", "benign.js", "runPing", Cap::CODE_EXEC, 14,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else { return; };
|
||||
assert_not_confirmed("async_function", &r);
|
||||
}
|
||||
|
||||
|
|
@ -138,21 +131,21 @@ mod javascript_fixture_tests {
|
|||
|
||||
#[test]
|
||||
fn esm_default_vuln_is_confirmed() {
|
||||
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
NODE_REQ,
|
||||
"esm_default", "vuln.js", "runPing", Cap::CODE_EXEC, 14,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else { return; };
|
||||
assert_confirmed("esm_default", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esm_default_benign_not_confirmed() {
|
||||
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
NODE_REQ,
|
||||
"esm_default", "benign.js", "runPing", Cap::CODE_EXEC, 14,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else { return; };
|
||||
assert_not_confirmed("esm_default", &r);
|
||||
}
|
||||
|
||||
|
|
@ -160,29 +153,27 @@ mod javascript_fixture_tests {
|
|||
|
||||
#[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(
|
||||
let Some(r) = run(
|
||||
&[
|
||||
Prerequisite::CommandAvailable("node"),
|
||||
Prerequisite::NodeModuleAvailable("express"),
|
||||
],
|
||||
"express", "vuln.js", "ping", Cap::CODE_EXEC, 15,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||||
);
|
||||
) else { return; };
|
||||
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(
|
||||
let Some(r) = run(
|
||||
&[
|
||||
Prerequisite::CommandAvailable("node"),
|
||||
Prerequisite::NodeModuleAvailable("express"),
|
||||
],
|
||||
"express", "benign.js", "ping", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||||
);
|
||||
) else { return; };
|
||||
assert_not_confirmed("express", &r);
|
||||
}
|
||||
|
||||
|
|
@ -190,29 +181,27 @@ mod javascript_fixture_tests {
|
|||
|
||||
#[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(
|
||||
let Some(r) = run(
|
||||
&[
|
||||
Prerequisite::CommandAvailable("node"),
|
||||
Prerequisite::NodeModuleAvailable("koa"),
|
||||
],
|
||||
"koa", "vuln.js", "ping", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||||
);
|
||||
) else { return; };
|
||||
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(
|
||||
let Some(r) = run(
|
||||
&[
|
||||
Prerequisite::CommandAvailable("node"),
|
||||
Prerequisite::NodeModuleAvailable("koa"),
|
||||
],
|
||||
"koa", "benign.js", "ping", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||||
);
|
||||
) else { return; };
|
||||
assert_not_confirmed("koa", &r);
|
||||
}
|
||||
|
||||
|
|
@ -220,29 +209,27 @@ mod javascript_fixture_tests {
|
|||
|
||||
#[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(
|
||||
let Some(r) = run(
|
||||
&[
|
||||
Prerequisite::CommandAvailable("node"),
|
||||
Prerequisite::NodeModuleAvailable("next"),
|
||||
],
|
||||
"next_route", "vuln.js", "handler", Cap::CODE_EXEC, 17,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||||
);
|
||||
) else { return; };
|
||||
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(
|
||||
let Some(r) = run(
|
||||
&[
|
||||
Prerequisite::CommandAvailable("node"),
|
||||
Prerequisite::NodeModuleAvailable("next"),
|
||||
],
|
||||
"next_route", "benign.js", "handler", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||||
);
|
||||
) else { return; };
|
||||
assert_not_confirmed("next_route", &r);
|
||||
}
|
||||
|
||||
|
|
@ -250,29 +237,27 @@ mod javascript_fixture_tests {
|
|||
|
||||
#[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(
|
||||
let Some(r) = run(
|
||||
&[
|
||||
Prerequisite::CommandAvailable("node"),
|
||||
Prerequisite::NodeModuleAvailable("jsdom"),
|
||||
],
|
||||
"browser_event", "vuln.js", "clickHandler", Cap::HTML_ESCAPE, 14,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else { return; };
|
||||
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(
|
||||
let Some(r) = run(
|
||||
&[
|
||||
Prerequisite::CommandAvailable("node"),
|
||||
Prerequisite::NodeModuleAvailable("jsdom"),
|
||||
],
|
||||
"browser_event", "benign.js", "clickHandler", Cap::HTML_ESCAPE, 14,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else { return; };
|
||||
assert_not_confirmed("browser_event", &r);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ use nyx_scanner::dynamic::lang::go::probe_shim as go_probe_shim;
|
|||
use nyx_scanner::dynamic::lang::javascript::probe_shim as node_probe_shim;
|
||||
use nyx_scanner::dynamic::lang::php::probe_shim as php_probe_shim;
|
||||
use nyx_scanner::dynamic::lang::python::probe_shim as python_probe_shim;
|
||||
use nyx_scanner::dynamic::lang::ruby::probe_shim as ruby_probe_shim;
|
||||
use nyx_scanner::dynamic::stubs::{HttpStub, SqlStub, StubProvider};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
|
@ -61,6 +62,14 @@ fn go_available() -> bool {
|
|||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn ruby_available() -> bool {
|
||||
Command::new("ruby")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Wrap the body-only Go HTTP fixture in a complete `package main`
|
||||
/// program: stdlib imports needed by the spliced probe shim plus the
|
||||
/// fragment's own `fmt` / `os` references, the shim itself, and the
|
||||
|
|
@ -793,6 +802,113 @@ fn go_http_shim_recorder_is_noop_without_log_env() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruby_http_stub_captures_attempted_outbound_via_shim_recorder() {
|
||||
// Phase 10 (Track D.3) HTTP recording: Ruby leg of the side-channel
|
||||
// `__nyx_stub_http_record` helper. Mirrors the Python HTTP test —
|
||||
// records an SSRF attempt without issuing the actual network call.
|
||||
// Ruby has no package / class boundary so the fixture is a plain
|
||||
// top-level script and the shim is prepended at the file head.
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = HttpStub::start(workdir.path()).expect("HttpStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let recording = stub
|
||||
.recording_endpoint()
|
||||
.expect("HttpStub must publish a recording endpoint");
|
||||
|
||||
let fixture =
|
||||
std::fs::read_to_string(fixture_path("ruby/http/vuln/main.rb")).expect("read fixture");
|
||||
let mut combined = String::with_capacity(ruby_probe_shim().len() + fixture.len() + 64);
|
||||
combined.push_str(ruby_probe_shim());
|
||||
combined.push_str("\n# ── fixture begins ─\n");
|
||||
combined.push_str(&fixture);
|
||||
|
||||
let script_path = workdir.path().join("driver_http.rb");
|
||||
std::fs::write(&script_path, combined).expect("write driver");
|
||||
|
||||
let output = Command::new("ruby")
|
||||
.arg(&script_path)
|
||||
.env("NYX_HTTP_ENDPOINT", &endpoint)
|
||||
.env(recording.0, &recording.1)
|
||||
.output()
|
||||
.expect("ruby driver");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0; stderr = {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let events = stub.drain_events();
|
||||
assert!(
|
||||
!events.is_empty(),
|
||||
"HttpStub must capture at least one event after the Ruby shim recorder fires"
|
||||
);
|
||||
let hit = events
|
||||
.iter()
|
||||
.find(|e| e.summary.contains("169.254.169.254"))
|
||||
.expect("recorded URL must contain the SSRF marker");
|
||||
assert_eq!(
|
||||
hit.detail.get("method").map(String::as_str),
|
||||
Some("GET"),
|
||||
"method detail must surface on the recorded event"
|
||||
);
|
||||
assert_eq!(
|
||||
hit.detail.get("url").map(String::as_str),
|
||||
Some("http://169.254.169.254/latest/meta-data/"),
|
||||
);
|
||||
assert_eq!(
|
||||
hit.detail.get("driver").map(String::as_str),
|
||||
Some("net/http"),
|
||||
"kwargs passed to __nyx_stub_http_record must surface as event detail entries"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruby_http_shim_recorder_is_noop_without_log_env() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = HttpStub::start(workdir.path()).expect("HttpStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let fixture =
|
||||
std::fs::read_to_string(fixture_path("ruby/http/vuln/main.rb")).expect("read fixture");
|
||||
let mut combined = String::new();
|
||||
combined.push_str(ruby_probe_shim());
|
||||
combined.push('\n');
|
||||
combined.push_str(&fixture);
|
||||
let script_path = workdir.path().join("driver_http_no_log.rb");
|
||||
std::fs::write(&script_path, combined).expect("write driver");
|
||||
|
||||
let output = Command::new("ruby")
|
||||
.arg(&script_path)
|
||||
.env("NYX_HTTP_ENDPOINT", &endpoint)
|
||||
.env_remove("NYX_HTTP_LOG")
|
||||
.output()
|
||||
.expect("ruby driver");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0 even without NYX_HTTP_LOG; stderr = {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let events = stub.drain_events();
|
||||
assert!(
|
||||
events.is_empty(),
|
||||
"no events expected when the recording env var is unset, got {} entries",
|
||||
events.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_sql_shim_recorder_is_noop_without_log_env() {
|
||||
if !node_available() {
|
||||
|
|
|
|||
|
|
@ -10,28 +10,16 @@ mod common;
|
|||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod typescript_fixture_tests {
|
||||
use crate::common::fixture_harness::run_shape_fixture_lang;
|
||||
use crate::common::fixture_harness::{run_shape_fixture_lang_or_skip, Prerequisite};
|
||||
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)
|
||||
}
|
||||
/// Base prereq slice shared by every TS shape: the host must have
|
||||
/// `node` on PATH. Framework-bound shapes extend the slice with a
|
||||
/// second `Prerequisite::NodeModuleAvailable("<pkg>")` entry.
|
||||
const NODE_REQ: &[Prerequisite] = &[Prerequisite::CommandAvailable("node")];
|
||||
|
||||
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert_eq!(
|
||||
|
|
@ -60,7 +48,9 @@ mod typescript_fixture_tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn run(
|
||||
requires: &[Prerequisite],
|
||||
shape: &str,
|
||||
file: &str,
|
||||
func: &str,
|
||||
|
|
@ -68,8 +58,9 @@ mod typescript_fixture_tests {
|
|||
sink_line: u32,
|
||||
kind: EntryKind,
|
||||
slot: PayloadSlot,
|
||||
) -> VerifyResult {
|
||||
run_shape_fixture_lang(
|
||||
) -> Option<VerifyResult> {
|
||||
run_shape_fixture_lang_or_skip(
|
||||
requires,
|
||||
Lang::TypeScript,
|
||||
"typescript",
|
||||
shape,
|
||||
|
|
@ -86,21 +77,21 @@ mod typescript_fixture_tests {
|
|||
|
||||
#[test]
|
||||
fn commonjs_export_vuln_is_confirmed() {
|
||||
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
NODE_REQ,
|
||||
"commonjs_export", "vuln.ts", "runPing", Cap::CODE_EXEC, 11,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else { return; };
|
||||
assert_confirmed("commonjs_export", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commonjs_export_benign_not_confirmed() {
|
||||
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
NODE_REQ,
|
||||
"commonjs_export", "benign.ts", "runPing", Cap::CODE_EXEC, 11,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else { return; };
|
||||
assert_not_confirmed("commonjs_export", &r);
|
||||
}
|
||||
|
||||
|
|
@ -108,21 +99,21 @@ mod typescript_fixture_tests {
|
|||
|
||||
#[test]
|
||||
fn async_function_vuln_is_confirmed() {
|
||||
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
NODE_REQ,
|
||||
"async_function", "vuln.ts", "runPing", Cap::CODE_EXEC, 15,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else { return; };
|
||||
assert_confirmed("async_function", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_function_benign_not_confirmed() {
|
||||
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
NODE_REQ,
|
||||
"async_function", "benign.ts", "runPing", Cap::CODE_EXEC, 14,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else { return; };
|
||||
assert_not_confirmed("async_function", &r);
|
||||
}
|
||||
|
||||
|
|
@ -130,21 +121,21 @@ mod typescript_fixture_tests {
|
|||
|
||||
#[test]
|
||||
fn esm_default_vuln_is_confirmed() {
|
||||
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
NODE_REQ,
|
||||
"esm_default", "vuln.ts", "runPing", Cap::CODE_EXEC, 14,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else { return; };
|
||||
assert_confirmed("esm_default", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esm_default_benign_not_confirmed() {
|
||||
if !node_available() { eprintln!("SKIP: node not available"); return; }
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
NODE_REQ,
|
||||
"esm_default", "benign.ts", "runPing", Cap::CODE_EXEC, 14,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else { return; };
|
||||
assert_not_confirmed("esm_default", &r);
|
||||
}
|
||||
|
||||
|
|
@ -152,29 +143,27 @@ mod typescript_fixture_tests {
|
|||
|
||||
#[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(
|
||||
let Some(r) = run(
|
||||
&[
|
||||
Prerequisite::CommandAvailable("node"),
|
||||
Prerequisite::NodeModuleAvailable("express"),
|
||||
],
|
||||
"express", "vuln.ts", "ping", Cap::CODE_EXEC, 15,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||||
);
|
||||
) else { return; };
|
||||
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(
|
||||
let Some(r) = run(
|
||||
&[
|
||||
Prerequisite::CommandAvailable("node"),
|
||||
Prerequisite::NodeModuleAvailable("express"),
|
||||
],
|
||||
"express", "benign.ts", "ping", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||||
);
|
||||
) else { return; };
|
||||
assert_not_confirmed("express", &r);
|
||||
}
|
||||
|
||||
|
|
@ -182,29 +171,27 @@ mod typescript_fixture_tests {
|
|||
|
||||
#[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(
|
||||
let Some(r) = run(
|
||||
&[
|
||||
Prerequisite::CommandAvailable("node"),
|
||||
Prerequisite::NodeModuleAvailable("koa"),
|
||||
],
|
||||
"koa", "vuln.ts", "ping", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||||
);
|
||||
) else { return; };
|
||||
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(
|
||||
let Some(r) = run(
|
||||
&[
|
||||
Prerequisite::CommandAvailable("node"),
|
||||
Prerequisite::NodeModuleAvailable("koa"),
|
||||
],
|
||||
"koa", "benign.ts", "ping", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||||
);
|
||||
) else { return; };
|
||||
assert_not_confirmed("koa", &r);
|
||||
}
|
||||
|
||||
|
|
@ -212,29 +199,27 @@ mod typescript_fixture_tests {
|
|||
|
||||
#[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(
|
||||
let Some(r) = run(
|
||||
&[
|
||||
Prerequisite::CommandAvailable("node"),
|
||||
Prerequisite::NodeModuleAvailable("next"),
|
||||
],
|
||||
"next_route", "vuln.ts", "handler", Cap::CODE_EXEC, 17,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||||
);
|
||||
) else { return; };
|
||||
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(
|
||||
let Some(r) = run(
|
||||
&[
|
||||
Prerequisite::CommandAvailable("node"),
|
||||
Prerequisite::NodeModuleAvailable("next"),
|
||||
],
|
||||
"next_route", "benign.ts", "handler", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
|
||||
);
|
||||
) else { return; };
|
||||
assert_not_confirmed("next_route", &r);
|
||||
}
|
||||
|
||||
|
|
@ -242,29 +227,27 @@ mod typescript_fixture_tests {
|
|||
|
||||
#[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(
|
||||
let Some(r) = run(
|
||||
&[
|
||||
Prerequisite::CommandAvailable("node"),
|
||||
Prerequisite::NodeModuleAvailable("jsdom"),
|
||||
],
|
||||
"browser_event", "vuln.ts", "clickHandler", Cap::HTML_ESCAPE, 14,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else { return; };
|
||||
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(
|
||||
let Some(r) = run(
|
||||
&[
|
||||
Prerequisite::CommandAvailable("node"),
|
||||
Prerequisite::NodeModuleAvailable("jsdom"),
|
||||
],
|
||||
"browser_event", "benign.ts", "clickHandler", Cap::HTML_ESCAPE, 14,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else { return; };
|
||||
assert_not_confirmed("browser_event", &r);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue