diff --git a/benches/dynamic_bench.rs b/benches/dynamic_bench.rs index 93584e32..dd010789 100644 --- a/benches/dynamic_bench.rs +++ b/benches/dynamic_bench.rs @@ -44,7 +44,9 @@ use criterion::{Criterion, criterion_group, criterion_main}; #[cfg(feature = "dynamic")] -use nyx_scanner::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy}; +use nyx_scanner::dynamic::spec::{ + EntryKind, HarnessSpec, JavaToolchain, PayloadSlot, SpecDerivationStrategy, +}; #[cfg(feature = "dynamic")] use nyx_scanner::labels::Cap; #[cfg(feature = "dynamic")] @@ -68,6 +70,7 @@ fn make_rust_sqli_spec() -> HarnessSpec { derivation: SpecDerivationStrategy::FromFlowSteps, stubs_required: vec![], framework: None, + java_toolchain: JavaToolchain::default(), } } @@ -89,6 +92,7 @@ fn make_sqli_spec() -> HarnessSpec { derivation: SpecDerivationStrategy::FromFlowSteps, stubs_required: vec![], framework: None, + java_toolchain: JavaToolchain::default(), } } @@ -288,6 +292,7 @@ fn make_js_sqli_spec() -> HarnessSpec { derivation: SpecDerivationStrategy::FromFlowSteps, stubs_required: vec![], framework: None, + java_toolchain: JavaToolchain::default(), } } @@ -309,6 +314,7 @@ fn make_go_sqli_spec() -> HarnessSpec { derivation: SpecDerivationStrategy::FromFlowSteps, stubs_required: vec![], framework: None, + java_toolchain: JavaToolchain::default(), } } @@ -330,6 +336,7 @@ fn make_java_sqli_spec() -> HarnessSpec { derivation: SpecDerivationStrategy::FromFlowSteps, stubs_required: vec![], framework: None, + java_toolchain: JavaToolchain::default(), } } @@ -351,6 +358,7 @@ fn make_php_sqli_spec() -> HarnessSpec { derivation: SpecDerivationStrategy::FromFlowSteps, stubs_required: vec![], framework: None, + java_toolchain: JavaToolchain::default(), } } diff --git a/src/dynamic/lang/ruby.rs b/src/dynamic/lang/ruby.rs index f9a2d2ad..91f644a4 100644 --- a/src/dynamic/lang/ruby.rs +++ b/src/dynamic/lang/ruby.rs @@ -723,6 +723,8 @@ fn emit_middleware_harness(handler: &str, name: &str) -> HarnessSource { r#"{preamble} puts "__NYX_MIDDLEWARE__: " + {name:?} +require 'stringio' + # Rack-shape middleware: class with #call(env). env = {{ 'REQUEST_METHOD' => 'POST', @@ -731,7 +733,6 @@ env = {{ 'rack.input' => StringIO.new($nyx_payload), 'nyx.payload' => $nyx_payload, }} -require 'stringio' if Object.const_defined?({handler:?}) cls = Object.const_get({handler:?}) @@ -1233,7 +1234,8 @@ fn invoke_for_shape(spec: &HarnessSpec, shape: RubyShape, entry_fn: &str) -> Str RubyShape::RackMiddleware => { let cls = entry_class_from_spec(spec); format!( - r#" cls = Object.const_defined?({cls:?}) ? Object.const_get({cls:?}) : nil + r#" require 'stringio' + cls = Object.const_defined?({cls:?}) ? Object.const_get({cls:?}) : nil if cls inner = cls.respond_to?(:new) ? (cls.method(:new).arity == 0 ? cls.new : cls.new(nil)) : nil env = {{ @@ -1243,7 +1245,6 @@ fn invoke_for_shape(spec: &HarnessSpec, shape: RubyShape, entry_fn: &str) -> Str 'rack.input' => StringIO.new(($nyx_request[:body] rescue '')), 'nyx.payload' => $nyx_payload, }} - require 'stringio' status, headers, body = inner.call(env) Array(body).each {{ |chunk| print(chunk.to_s) }} end"#, diff --git a/src/dynamic/sandbox/mod.rs b/src/dynamic/sandbox/mod.rs index 6334b6bf..042978e3 100644 --- a/src/dynamic/sandbox/mod.rs +++ b/src/dynamic/sandbox/mod.rs @@ -775,7 +775,20 @@ pub fn run( } } SandboxBackend::Auto => { - if docker_available() && harness_is_interpreted(&harness.command) { + // Docker containers run the interpreter image's bare runtime + // (python:3-slim, node:20-slim, ruby:3-slim, ...) with no + // network access under NetworkPolicy::None. Harness shapes + // that depend on packages declared via requirements.txt / + // package.json / Gemfile / composer.json can be served from + // the host build cache by prepare_*, but the container has + // no way to fetch them at exec time. Route to the process + // backend in that case so the harness picks up the host + // venv / node_modules / vendor dir already prepared. + let needs_host_deps = harness_needs_host_deps(harness); + if docker_available() + && harness_is_interpreted(&harness.command) + && !needs_host_deps + { run_docker(harness, payload_bytes, opts) } else if docker_available() && harness_is_native_binary(&harness.command) { run_native_binary_docker(harness, payload_bytes, opts) @@ -788,6 +801,33 @@ pub fn run( } } +/// True when the harness workdir carries a dependency manifest that the +/// docker backend has no mechanism to materialise inside the container. +/// +/// `prepare_python` / `prepare_node` / `prepare_php` / etc. resolve these +/// against the host build cache before the run, so the process backend +/// already has a fully-populated venv / node_modules / vendor dir to +/// invoke. The docker backend, on the other hand, mounts the workdir +/// into a bare interpreter image (python:3-slim, node:20-slim, ...) and +/// runs under `--network=none`, leaving no path for an in-container +/// `pip install` / `npm install` / `composer install` to fetch the deps. +/// Routing those shapes to the process backend keeps the verifier honest +/// on dev hosts where docker is available but the bare image lacks the +/// third-party libs the entry source imports. +fn harness_needs_host_deps(harness: &BuiltHarness) -> bool { + const MANIFESTS: &[&str] = &[ + "requirements.txt", + "Pipfile.lock", + "pyproject.toml", + "package.json", + "Gemfile", + "composer.json", + ]; + MANIFESTS + .iter() + .any(|name| harness.workdir.join(name).exists()) +} + /// Phase 20 (Track E.4): dispatch the Firecracker backend. /// /// When `--features firecracker` is off, the call returns diff --git a/tests/xpath_corpus.rs b/tests/xpath_corpus.rs index bc5cc601..febd98ac 100644 --- a/tests/xpath_corpus.rs +++ b/tests/xpath_corpus.rs @@ -424,7 +424,8 @@ mod e2e_phase_07 { Lang::Java => "java", Lang::Python => "python3", Lang::Php => "php", - _ => unreachable!("e2e_phase_07 covers Java/Python/PHP"), + Lang::JavaScript => "node", + _ => unreachable!("e2e_phase_07 covers Java/Python/PHP/JS"), } } @@ -433,6 +434,7 @@ mod e2e_phase_07 { Lang::Java => "java", Lang::Python => "python", Lang::Php => "php", + Lang::JavaScript => "js", _ => unreachable!(), } } @@ -549,4 +551,18 @@ mod e2e_phase_07 { .expect("Confirmed run must carry a DifferentialOutcome"); assert_eq!(diff.verdict, DifferentialVerdict::Confirmed); } + + #[test] + fn javascript_vuln_confirms_via_run_spec() { + let Some(outcome) = run(Lang::JavaScript, "vuln.js", "run") else { return }; + assert!( + outcome.triggered_by.is_some(), + "JavaScript XPath vuln must Confirm via run_spec; got {outcome:?}", + ); + let diff = outcome + .differential + .as_ref() + .expect("Confirmed run must carry a DifferentialOutcome"); + assert_eq!(diff.verdict, DifferentialVerdict::Confirmed); + } } diff --git a/tests/xxe_corpus.rs b/tests/xxe_corpus.rs index 607a1b5b..9c9205a5 100644 --- a/tests/xxe_corpus.rs +++ b/tests/xxe_corpus.rs @@ -354,7 +354,8 @@ mod e2e_phase_05 { Lang::Python => "python3", Lang::Php => "php", Lang::Ruby => "ruby", - _ => unreachable!("e2e_phase_05 covers Java/Python/PHP/Ruby"), + Lang::Go => "go", + _ => unreachable!("e2e_phase_05 covers Java/Python/PHP/Ruby/Go"), } } @@ -364,6 +365,7 @@ mod e2e_phase_05 { Lang::Python => "python", Lang::Php => "php", Lang::Ruby => "ruby", + Lang::Go => "go", _ => unreachable!(), } } @@ -494,4 +496,18 @@ mod e2e_phase_05 { .expect("Confirmed run must carry a DifferentialOutcome"); assert_eq!(diff.verdict, DifferentialVerdict::Confirmed); } + + #[test] + fn go_vuln_confirms_via_run_spec() { + let Some(outcome) = run(Lang::Go, "vuln.go", "run") else { return }; + assert!( + outcome.triggered_by.is_some(), + "Go XXE vuln must Confirm via run_spec; got {outcome:?}", + ); + let diff = outcome + .differential + .as_ref() + .expect("Confirmed run must carry a DifferentialOutcome"); + assert_eq!(diff.verdict, DifferentialVerdict::Confirmed); + } }