//! Java harness emitter. //! //! Phase 14 (Track B Java vertical) replaces the single legacy `emit` //! body with dispatch over [`JavaShape`] — the cross product of //! [`EntryKind`] and a lightweight per-file shape detector that inspects //! the entry file for servlet / Spring / Quarkus annotations, JUnit //! markers, and `static main(String[])` signatures. //! //! Each shape emits a single `NyxHarness.java` that: //! 1. Reads the payload from `NYX_PAYLOAD` / `NYX_PAYLOAD_B64`. //! 2. Locates the entry class (default-package, derived from the entry //! file basename) and invokes its method via the per-shape adapter. //! 3. Catches all exceptions so the JVM exit shape stays observable. //! //! Sink-reachability probe: fixtures explicitly emit //! `System.out.println("__NYX_SINK_HIT__")` before the actual sink call //! (same pattern as Rust and Go fixtures). //! //! Build step: `prepare_java()` in `build_sandbox.rs` runs `javac` over //! every `*.java` file in the workdir. Shape fixtures bundle their own //! annotation / type stubs (e.g. a minimal `HttpServletRequest.java` //! when the shape needs servlet plumbing) so the JDK can compile the //! source without pulling Maven dependencies. //! //! Payload slot support: //! - [`PayloadSlot::Param`] — pass payload as `String` first argument //! (n-th positional for `Param(n)` where `n > 0`). //! - [`PayloadSlot::EnvVar`] — set a system property before invocation. //! - [`PayloadSlot::QueryParam`] / [`PayloadSlot::HttpBody`] — surfaced //! to servlet / Spring / Quarkus adapters as the request body or //! query parameter value. //! - [`PayloadSlot::Argv`] — appended to a `String[] args` for //! `static main` shapes. //! - Other slots produce [`UnsupportedReason::PayloadSlotUnsupported`]. //! //! Build container: `nyx-build-java:{toolchain_id}` (deferred; §19.1). use crate::dynamic::environment::{Environment, RuntimeArtifacts}; use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter}; use crate::dynamic::spec::{EntryKindTag, HarnessSpec, PayloadSlot}; use crate::evidence::UnsupportedReason; use std::path::PathBuf; /// Zero-sized [`LangEmitter`] handle for Java. Method bodies delegate to the /// existing free functions in this module. pub struct JavaEmitter; /// Entry kinds the Java emitter understands after Phase 14. /// /// `HttpRoute` covers servlet / Spring / Quarkus shapes. `CliSubcommand` /// covers `public static void main(String[])`. `Function` covers JUnit /// tests and plain static methods. const SUPPORTED: &[EntryKindTag] = &[ EntryKindTag::Function, EntryKindTag::HttpRoute, EntryKindTag::CliSubcommand, EntryKindTag::ClassMethod, EntryKindTag::MessageHandler, EntryKindTag::ScheduledJob, EntryKindTag::Middleware, ]; impl LangEmitter for JavaEmitter { fn emit(&self, spec: &HarnessSpec) -> Result { emit(spec) } fn entry_kinds_supported(&self) -> &'static [EntryKindTag] { SUPPORTED } fn entry_kind_hint(&self, attempted: EntryKindTag) -> String { format!( "java emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 14 / 19 / 20 / 21 shape dispatch" ) } fn materialize_runtime(&self, env: &Environment) -> RuntimeArtifacts { materialize_java(env) } fn compose_chain_step( &self, prev_output: Option<&[u8]>, terminal: Option<&ChainStepTerminal>, ) -> ChainStepHarness { chain_step(prev_output, terminal) } } /// Phase 26 — Java chain-step harness. /// /// Emits a `Step.java` class whose `main` reads `NYX_PREV_OUTPUT` and /// forwards it on stdout. When the step is the chain's terminal step /// the `main` body also calls `__nyx_probe(callee, prev)` and prints /// [`ChainStepHarness::SINK_HIT_SENTINEL`] so the runner flips /// `sink_hit` for the chain. The command shell-wraps `javac` + `java` /// so the step actually runs after the build step completes (the /// `ChainStepHarness.command` slot models a single process). /// /// The Java probe shim (`__nyx_probe`, `__nyx_install_crash_guard`, /// helpers) is spliced as class-member declarations inside `class Step /// { … }` between the class-open brace and `public static void main`, /// so a downstream sink rewrite within the step body has the shim /// helpers already in scope. The shim uses only `java.lang.*` plus /// fully-qualified `java.util.TreeMap` / `java.io.FileWriter` / /// `java.nio.charset.StandardCharsets`, so no extra `import` lines /// are needed beyond what stock Java implicitly imports. fn chain_step( prev_output: Option<&[u8]>, terminal: Option<&ChainStepTerminal>, ) -> ChainStepHarness { let shim = probe_shim(); let mut body = String::from( " String prev = System.getenv(\"NYX_PREV_OUTPUT\");\n if (prev == null) prev = \"\";\n System.out.print(prev);\n", ); if let Some(t) = terminal { let callee = java_string_literal(&t.sink_callee); let sentinel = java_string_literal(ChainStepHarness::SINK_HIT_SENTINEL); body.push_str(&format!( " __nyx_probe({callee}, prev);\n System.out.println({sentinel});\n System.out.flush();\n", )); } let source = format!( "public class Step {{\n{shim}\n public static void main(String[] args) {{\n{body} }}\n}}\n" ); ChainStepHarness { source, filename: "Step.java".to_owned(), command: vec![ "sh".to_owned(), "-c".to_owned(), "javac Step.java && java Step".to_owned(), ], extra_env: prev_output .map(|bytes| { vec![( ChainStepHarness::PREV_OUTPUT_ENV.to_owned(), String::from_utf8_lossy(bytes).into_owned(), )] }) .unwrap_or_default(), extra_files: Vec::new(), } } /// Escape a string for safe Java double-quoted literal embedding. fn java_string_literal(s: &str) -> String { let escaped = s.replace('\\', "\\\\").replace('"', "\\\""); format!("\"{escaped}\"") } // ── Phase 14: shape detector ───────────────────────────────────────────────── /// Concrete per-file shape resolved by reading the entry source. /// /// One harness template per variant. When the entry file is unreadable /// or no marker fires the detector defaults to [`JavaShape::StaticMethod`], /// which preserves the pre-Phase-14 behaviour (direct static method call). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum JavaShape { /// `public class … extends HttpServlet { void doGet(req, resp) }`. /// Harness instantiates the class via the default constructor and /// invokes `doGet` with a minimal `HttpServletRequest` / `Response` /// stub-pair via reflection. ServletDoGet, /// `void doPost(req, resp)` variant. Same adapter shape as doGet /// but uses `POST` semantics for query-vs-body wiring. ServletDoPost, /// Spring `@RestController` / `@Controller` with a `@RequestMapping` /// / `@GetMapping` / `@PostMapping` handler. Harness instantiates /// the controller via reflection (default ctor) and invokes the /// handler method with the payload routed into the matching /// `String` parameter. SpringController, /// `public static void main(String[] args)`. Harness calls /// `Class.forName(name).getMethod("main", String[].class)` and /// passes a one-element argv populated from the payload. StaticMain, /// JUnit 4 (`@Test`) or JUnit 5 (`@Test` from `org.junit.jupiter.api`). /// Harness instantiates the test class and invokes the annotated /// method via reflection — no JUnit runner needed since we drive a /// single test method. JunitTest, /// Quarkus reactive route: `@Path("/foo")` + `@GET`/`@POST` on a /// method. Harness invokes the method via reflection like Spring. QuarkusRoute, /// Micronaut route: `@Controller("/api")` + `@Get`/`@Post`/`@Put` /// /`@Delete` on a method. Harness invokes the method via /// reflection like Spring / Quarkus (the brief specifies an /// `EmbeddedServer.start` bootstrap, deferred behind the existing /// synthetic-harness pattern in [`deferred.md`]). MicronautRoute, /// Plain static method — legacy default behaviour from before /// Phase 14. Harness directly calls `{Class}.{method}(payload)`. StaticMethod, } impl JavaShape { /// Detect the shape from `(spec, source)`. `source` is the literal /// bytes of the entry file (best-effort — if it could not be read, /// pass an empty string and the function returns /// [`Self::StaticMethod`]). /// /// Framework / annotation detection wins over the [`EntryKind`] /// axis: when the source clearly imports a servlet or Spring /// controller the shape is selected even if the spec derivation /// pipeline tagged the entry kind as [`EntryKind::Function`]. pub fn detect(spec: &HarnessSpec, source: &str) -> Self { let entry = spec.entry_name.as_str(); let kind = spec.entry_kind.tag(); let has_servlet = source.contains("HttpServlet") || source.contains("javax.servlet") || source.contains("jakarta.servlet"); let has_spring_controller = source.contains("@RestController") || source.contains("@Controller") || source.contains("@RequestMapping") || source.contains("@GetMapping") || source.contains("@PostMapping"); let has_quarkus = source.contains("@Path(") || source.contains("io.quarkus") || source.contains("jakarta.ws.rs"); let has_micronaut = source.contains("io.micronaut"); let has_junit = source.contains("@Test") && (source.contains("org.junit") || source.contains("junit.framework")); let has_main = entry == "main" || source.contains("static void main("); // Servlet beats Spring when both fire (e.g. a Spring app that // mounts a raw servlet) — the doGet/doPost signature is more // specific. if has_servlet { if entry == "doPost" || source.contains("void doPost(") { return Self::ServletDoPost; } if entry == "doGet" || source.contains("void doGet(") { return Self::ServletDoGet; } return Self::ServletDoGet; } // Micronaut comes before Quarkus / Spring: Micronaut sources // re-use `@Controller` (collides with Spring) and `@Path` is // not part of the Micronaut surface (so the Quarkus check // does not fire for typical Micronaut files). Picking // Micronaut on a clear `io.micronaut` import is the safest // disambiguation. if has_micronaut { return Self::MicronautRoute; } if has_quarkus { return Self::QuarkusRoute; } if has_spring_controller { return Self::SpringController; } if has_main { return Self::StaticMain; } if has_junit { return Self::JunitTest; } if kind == EntryKindTag::CliSubcommand { return Self::StaticMain; } if kind == EntryKindTag::HttpRoute { return Self::SpringController; } Self::StaticMethod } } // (Helper retired in Phase 14 — the shape detector now uses direct // `source.contains` matches against the method-signature head because // the JDK accepts whitespace / newline / modifier variation that no // single template captures.) // ── Probe shim (Phase 06 + Phase 08) ───────────────────────────────────────── /// Source of the `__nyx_probe` shim for the Java harness (Phase 06 — /// Track C.1). /// /// Splices into the generated harness class as a `static void __nyx_probe(...)` /// method. Hand-rolled JSON keeps the shim free of org.json / jackson /// dependencies; matches the /// [`crate::dynamic::probe::SinkProbe`] wire format. pub fn probe_shim() -> &'static str { r##" // ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ── private static final String[] __NYX_DENY = { "TOKEN","SECRET","PASSWORD","PASSWD","API_KEY","APIKEY","PRIVATE_KEY", "CREDENTIAL","SESSION","COOKIE","AUTH","BEARER","AWS_ACCESS","AWS_SESSION", "GH_TOKEN","GITHUB_TOKEN","NPM_TOKEN","PYPI_TOKEN","DOCKER_PASS" }; private static final int __NYX_PAYLOAD_LIMIT = 16 * 1024; private static final String __NYX_REDACTED = ""; private static boolean nyxIsDeniedKey(String k) { String ku = k.toUpperCase(); for (String n : __NYX_DENY) { if (ku.contains(n)) return true; } return false; } private static String nyxWitnessJson(String sinkCallee, String[] args) { StringBuilder out = new StringBuilder(256); out.append("{\"env_snapshot\":{"); boolean first = true; java.util.TreeMap envSorted = new java.util.TreeMap<>(System.getenv()); for (java.util.Map.Entry e : envSorted.entrySet()) { if (!first) out.append(','); first = false; out.append('"'); nyxJsonEscape(e.getKey(), out); out.append("\":\""); if (nyxIsDeniedKey(e.getKey())) { out.append(__NYX_REDACTED); } else { nyxJsonEscape(e.getValue() == null ? "" : e.getValue(), out); } out.append('"'); } out.append("},\"cwd\":\""); nyxJsonEscape(System.getProperty("user.dir", ""), out); out.append("\",\"payload_bytes\":["); String payload = System.getenv("NYX_PAYLOAD"); if (payload != null) { byte[] pb = payload.getBytes(java.nio.charset.StandardCharsets.UTF_8); int cap = Math.min(pb.length, __NYX_PAYLOAD_LIMIT); for (int i = 0; i < cap; i++) { if (i > 0) out.append(','); out.append(((int) pb[i]) & 0xff); } } out.append("],\"callee\":\""); nyxJsonEscape(sinkCallee, out); out.append("\",\"args_repr\":["); if (args != null) { for (int i = 0; i < args.length; i++) { if (i > 0) out.append(','); out.append('"'); nyxJsonEscape(args[i] == null ? "" : args[i], out); out.append('"'); } } out.append("]}"); return out.toString(); } private static void nyxEmit(String line) { String p = System.getenv("NYX_PROBE_PATH"); if (p == null || p.isEmpty()) return; try (java.io.FileWriter fw = new java.io.FileWriter(p, true)) { fw.write(line); } catch (java.io.IOException e) { // best-effort } } static void __nyx_probe(String sinkCallee, String... args) { long now = System.nanoTime(); String payloadId = System.getenv("NYX_PAYLOAD_ID"); if (payloadId == null) payloadId = ""; StringBuilder line = new StringBuilder(256); line.append("{\"sink_callee\":\""); nyxJsonEscape(sinkCallee, line); line.append("\",\"args\":["); for (int i = 0; i < args.length; i++) { if (i > 0) line.append(','); line.append("{\"kind\":\"String\",\"value\":\""); nyxJsonEscape(args[i] == null ? "" : args[i], line); line.append("\"}"); } line.append("],\"captured_at_ns\":").append(now).append(",\"payload_id\":\""); nyxJsonEscape(payloadId, line); line.append("\",\"kind\":{\"kind\":\"Normal\"},\"witness\":"); line.append(nyxWitnessJson(sinkCallee, args)); line.append("}\n"); nyxEmit(line.toString()); } // Phase 08: install a sink-site Throwable handler. Java cannot catch // SIGSEGV / SIGFPE directly (JVM aborts), but it can intercept the // uncaught-exception path which fires for any Error / RuntimeException // escaping the sink call. Map them onto SIGABRT for the oracle. static void __nyx_install_crash_guard(String sinkCallee) { Thread.setDefaultUncaughtExceptionHandler((t, e) -> { long now = System.nanoTime(); String payloadId = System.getenv("NYX_PAYLOAD_ID"); if (payloadId == null) payloadId = ""; StringBuilder line = new StringBuilder(256); line.append("{\"sink_callee\":\""); nyxJsonEscape(sinkCallee, line); line.append("\",\"args\":[],\"captured_at_ns\":").append(now) .append(",\"payload_id\":\""); nyxJsonEscape(payloadId, line); line.append("\",\"kind\":{\"kind\":\"Crash\",\"signal\":\"SIGABRT\"},\"witness\":"); line.append(nyxWitnessJson(sinkCallee, new String[0])); line.append("}\n"); nyxEmit(line.toString()); System.exit(134); }); } private static void nyxJsonEscape(String s, StringBuilder out) { for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); switch (c) { case '"': out.append("\\\""); break; case '\\': out.append("\\\\"); break; case '\n': out.append("\\n"); break; case '\r': out.append("\\r"); break; case '\t': out.append("\\t"); break; default: if (c < 0x20) { out.append(String.format("\\u%04x", (int) c)); } else { out.append(c); } } } } // 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 / Ruby siblings so the host-side HttpStub // log-line merger parses all six 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. The hash prefix is emitted via // String.valueOf('#') so this method body contains no literal hash-after- // double-quote sequence that would terminate the surrounding Rust raw // string. static void __nyx_stub_http_record(String method, String url, String body, java.util.Map detail) { String p = System.getenv("NYX_HTTP_LOG"); if (p == null || p.isEmpty()) return; String hashSp = String.valueOf('#') + " "; try (java.io.FileWriter fw = new java.io.FileWriter(p, true)) { fw.write(hashSp + "method: " + method + "\n"); fw.write(hashSp + "url: " + url + "\n"); if (body != null) { fw.write(hashSp + "body: " + body + "\n"); } if (detail != null) { for (java.util.Map.Entry e : detail.entrySet()) { fw.write(hashSp + e.getKey() + ": " + e.getValue() + "\n"); } } fw.write(method + " " + url + "\n"); } catch (java.io.IOException e) { // best-effort } } // Phase 10 (Track D.3) SQL recording helper. When the verifier spawned a // SqlStub it publishes the side-channel log path through NYX_SQL_LOG; a // sink call site whose query never reaches the on-the-wire SQLite engine // (e.g. classpath lacks sqlite-jdbc, or the harness pre-flights the SQL // string before opening the connection) can call this helper to surface // the attempted query. Hash-prefixed detail lines followed by the query // line so SqlStub::drain_events parses every language stream identically. // Same hash-via-String.valueOf trick as __nyx_stub_http_record so this // method body contains no literal `"#` sequence that would terminate the // surrounding Rust raw string. static void __nyx_stub_sql_record(String query, java.util.Map detail) { String p = System.getenv("NYX_SQL_LOG"); if (p == null || p.isEmpty()) return; String hashSp = String.valueOf('#') + " "; try (java.io.FileWriter fw = new java.io.FileWriter(p, true)) { if (detail != null) { for (java.util.Map.Entry e : detail.entrySet()) { fw.write(hashSp + e.getKey() + ": " + e.getValue() + "\n"); } } fw.write(query); if (!query.endsWith("\n")) { fw.write("\n"); } } catch (java.io.IOException e) { // best-effort } } "## } // ── Runtime / pom.xml synthesis (Phase 09) ────────────────────────────────── /// Phase 09 — Track D.2: synthesise a minimal `pom.xml` that pins the /// Java toolchain and lists the direct dep top-level packages as /// dependencies. Each direct dep maps to `{pkg}` /// with an artifact id matching the package name; this is a best-effort /// stub and Phase 10 corpus expansion will introduce a known-good /// group→artifact registry. pub fn materialize_java(env: &Environment) -> RuntimeArtifacts { let mut artifacts = RuntimeArtifacts::new(); let java_version = env .toolchain .version_string .split('.') .next() .unwrap_or("21") .to_owned(); let mut deps: Vec = Vec::new(); let mut seen: std::collections::HashSet = std::collections::HashSet::new(); for d in &env.direct_deps { if is_java_stdlib(d) { continue; } if seen.insert(d.clone()) { deps.push(d.clone()); } } deps.sort_unstable(); let mut body = String::with_capacity(256); body.push_str("\n"); body.push_str("\n"); body.push_str(" 4.0.0\n"); body.push_str(" nyx\n"); body.push_str(" harness\n"); body.push_str(" 0.0.1\n"); body.push_str(" \n"); body.push_str(&format!( " {java_version}\n" )); body.push_str(&format!( " {java_version}\n" )); body.push_str(" \n"); if !deps.is_empty() { body.push_str(" \n"); for d in &deps { body.push_str(" \n"); body.push_str(&format!(" {d}\n")); body.push_str(&format!(" {d}\n")); body.push_str(" LATEST\n"); body.push_str(" \n"); } body.push_str(" \n"); } body.push_str("\n"); artifacts.push("pom.xml", body); artifacts } fn is_java_stdlib(name: &str) -> bool { // Best-effort: only `java` / `javax` / `sun` are guaranteed JDK. // `jakarta` ships separately under Jakarta EE so it stays out. // Top-level segments `com` / `org` cover both JDK (`com.sun`) and // third-party (`com.google`, `org.springframework`) — the import // extractor only keeps the first segment, so a richer registry has // to land before we can pin a meaningful Maven artifact from these. // Phase 10 corpus expansion ships that registry. matches!(name, "java" | "javax" | "sun" | "com" | "org" | "jakarta") } // ── Public entry: emit() ──────────────────────────────────────────────────── /// Emit a Java harness for `spec`. /// /// Reads `spec.entry_file` from disk (best-effort), resolves the /// concrete [`JavaShape`] via [`JavaShape::detect`], and dispatches to /// the matching per-shape emitter. When the file cannot be read the /// dispatcher falls back to [`JavaShape::StaticMethod`], preserving the /// pre-Phase-14 behaviour. pub fn emit(spec: &HarnessSpec) -> Result { match &spec.payload_slot { PayloadSlot::Param(_) | PayloadSlot::EnvVar(_) | PayloadSlot::QueryParam(_) | PayloadSlot::HttpBody | PayloadSlot::Argv(_) => {} PayloadSlot::Stdin => return Err(UnsupportedReason::PayloadSlotUnsupported), } if spec.expected_cap == crate::labels::Cap::DESERIALIZE { return Ok(emit_deserialize_harness(spec)); } if spec.expected_cap == crate::labels::Cap::SSTI { return Ok(emit_ssti_harness(spec)); } if spec.expected_cap == crate::labels::Cap::XXE { return Ok(emit_xxe_harness(spec)); } if spec.expected_cap == crate::labels::Cap::LDAP_INJECTION { return Ok(emit_ldap_harness(spec)); } if spec.expected_cap == crate::labels::Cap::XPATH_INJECTION { return Ok(emit_xpath_harness(spec)); } if spec.expected_cap == crate::labels::Cap::HEADER_INJECTION { return Ok(emit_header_injection_harness(spec)); } if spec.expected_cap == crate::labels::Cap::OPEN_REDIRECT { return Ok(emit_open_redirect_harness(spec)); } // Phase 19 (Track M.1): ClassMethod short-circuit. Routes through // the existing `invokeReflective` helper so the harness instantiates // the receiver via its no-arg constructor (or null-fills primitive // / null-safe-object formals) before dispatching `method(payload)`. if let crate::evidence::EntryKind::ClassMethod { class, method } = &spec.entry_kind { let entry_source = read_entry_source(&spec.entry_file); let entry_class = derive_entry_class(&entry_source); return Ok(emit_class_method_harness(spec, class, method, &entry_class)); } // Phase 20 (Track M.2): MessageHandler short-circuit. Mounts the // in-process broker loopback declared by `broker_{kafka,sqs,rabbit}` // and dispatches the payload synchronously to the named handler. if let crate::evidence::EntryKind::MessageHandler { queue, .. } = &spec.entry_kind { let entry_source = read_entry_source(&spec.entry_file); let entry_class = derive_entry_class(&entry_source); return Ok(emit_message_handler_harness(spec, queue, &entry_class)); } // Phase 21 (Track M.3): ScheduledJob short-circuit (Quartz). if let crate::evidence::EntryKind::ScheduledJob { schedule } = &spec.entry_kind { let entry_source = read_entry_source(&spec.entry_file); let entry_class = derive_entry_class(&entry_source); return Ok(emit_scheduled_job_harness( spec, schedule.as_deref(), &entry_class, )); } // Phase 21 (Track M.3): Middleware short-circuit (Spring HandlerInterceptor / Filter). if let crate::evidence::EntryKind::Middleware { name } = &spec.entry_kind { let entry_source = read_entry_source(&spec.entry_file); let entry_class = derive_entry_class(&entry_source); return Ok(emit_middleware_harness(spec, name, &entry_class)); } let entry_source = read_entry_source(&spec.entry_file); let shape = JavaShape::detect(spec, &entry_source); let entry_class = derive_entry_class(&entry_source); let entry_qualifier = derive_entry_qualifier(&entry_source, &entry_class); let source = generate_harness_java(spec, shape, &entry_qualifier); let mut extra_files = match shape { // Real-world servlet sources import `javax.servlet.*` or // `jakarta.servlet.*`; without those symbols on the classpath // `javac` reports `package javax.servlet does not exist` and the // verifier flips to `BuildFailed`. Stage minimal stubs alongside // the harness so the build step links. JavaShape::ServletDoGet | JavaShape::ServletDoPost => { crate::dynamic::lang::java_servlet_stubs::servlet_stub_files() } _ => vec![], }; // OWASP Benchmark v1.2 fixtures and other Spring-flavoured Java // entry sources reach for `org.owasp.benchmark.helpers.*`, // `org.owasp.esapi.*`, and a small Spring surface (RowMapper, // SqlRowSet, DataAccessException, HtmlUtils). Stage the matching // stub bundle when the entry source signals one of those imports; // non-OWASP harnesses pay zero workdir cost. if crate::dynamic::lang::java_owasp_stubs::entry_needs_owasp_stubs(&entry_source) { extra_files.extend(crate::dynamic::lang::java_owasp_stubs::owasp_stub_files()); } Ok(HarnessSource { source, filename: "NyxHarness.java".to_owned(), command: vec![ "java".to_owned(), "-cp".to_owned(), ".".to_owned(), "NyxHarness".to_owned(), ], extra_files, // Stage the entry file under the public-class-derived filename // so javac's filename-vs-public-class invariant holds for both // the legacy `public class Entry` fixtures (which keep being // copied to `workdir/Entry.java`) and the Phase 14 shape // fixtures (where `public class Vuln` lives in `Vuln.java`). entry_subpath: Some(format!("{entry_class}.java")), }) } /// Phase 03 — Track J.1 deserialize harness for Java. /// /// Forges a minimal valid Java serialization stream for the marker /// class name carried by `NYX_PAYLOAD`, then runs it through a /// `RestrictedObjectInputStream` subclass whose `resolveClass` override /// enforces a static allowlist (`java.lang.Integer`, `java.lang.String`). /// When `resolveClass` sees a non-allowlisted class it writes a /// [`crate::dynamic::probe::ProbeKind::Deserialize`] probe with /// `gadget_chain_invoked: true` and throws `InvalidClassException` to /// abort — matching the JEP-290 / Look-Ahead-OIS hardening pattern /// real applications use. The blob is built from raw stream bytes /// (TC_OBJECT → TC_CLASSDESC → class name → SUID → flags → no /// fields → TC_ENDBLOCKDATA → TC_NULL super) so the resolveClass /// boundary fires for both vuln and benign payloads; downstream /// instantiation failures (e.g. `serialVersionUID` mismatch on the /// allow-listed payload) are caught and treated as non-probe paths. pub fn emit_deserialize_harness(_spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); let source = format!( r#"// Nyx dynamic harness — deserialize (Phase 03 / Track J.1). import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InvalidClassException; import java.io.ObjectInputStream; import java.io.ObjectStreamClass; import java.util.Arrays; import java.util.HashSet; import java.util.Set; public class NyxHarness {{ {shim} static final Set NYX_ALLOWLIST = new HashSet<>(Arrays.asList("java.lang.Integer", "java.lang.String")); static void nyxDeserializeProbe(boolean invoked) {{ String p = System.getenv("NYX_PROBE_PATH"); if (p == null || p.isEmpty()) return; long now = System.nanoTime(); String pid = System.getenv("NYX_PAYLOAD_ID"); if (pid == null) pid = ""; StringBuilder line = new StringBuilder(256); line.append("{{\"sink_callee\":\"ObjectInputStream.resolveClass\",\"args\":[],"); line.append("\"captured_at_ns\":").append(now).append(','); line.append("\"payload_id\":\""); nyxJsonEscape(pid, line); line.append("\",\"kind\":{{\"kind\":\"Deserialize\",\"gadget_chain_invoked\":").append(invoked ? "true" : "false").append("}},"); line.append("\"witness\":"); line.append(nyxWitnessJson("ObjectInputStream.resolveClass", new String[0])); line.append("}}\n"); try (FileWriter fw = new FileWriter(p, true)) {{ fw.write(line.toString()); }} catch (IOException e) {{ // best-effort }} }} static class NyxRestrictedOIS extends ObjectInputStream {{ NyxRestrictedOIS(InputStream in) throws IOException {{ super(in); }} @Override protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {{ String name = desc.getName(); if (!NYX_ALLOWLIST.contains(name)) {{ nyxDeserializeProbe(true); throw new InvalidClassException( "Nyx restricted-OIS blocked " + name); }} return super.resolveClass(desc); }} }} static byte[] nyxForgeClassDescriptor(String className) throws IOException {{ ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos); dos.writeShort((short) 0xACED); // STREAM_MAGIC dos.writeShort((short) 0x0005); // STREAM_VERSION dos.writeByte(0x73); // TC_OBJECT dos.writeByte(0x72); // TC_CLASSDESC dos.writeUTF(className); dos.writeLong(0L); // serialVersionUID dos.writeByte(0x02); // SC_SERIALIZABLE dos.writeShort(0); // 0 fields dos.writeByte(0x78); // TC_ENDBLOCKDATA dos.writeByte(0x70); // TC_NULL (no super class) return baos.toByteArray(); }} public static void main(String[] args) {{ String payload = System.getenv("NYX_PAYLOAD"); if (payload == null) payload = ""; String prefix = "NYX_GADGET_CLASS:"; if (payload.startsWith(prefix)) {{ String cls = payload.substring(prefix.length()); try {{ byte[] blob = nyxForgeClassDescriptor(cls); NyxRestrictedOIS ois = new NyxRestrictedOIS( new ByteArrayInputStream(blob)); try {{ ois.readObject(); }} finally {{ try {{ ois.close(); }} catch (IOException ignored) {{}} }} }} catch (InvalidClassException e) {{ // Restricted block — probe already written above. }} catch (Throwable t) {{ // Allow-listed but downstream instantiation fails (the // minimal stream omits the field bytes the real class // expects). resolveClass already fired; treat as a // non-probe path. }} }} // Sink-reachability sentinel — runner's `vuln_fired && sink_hit` // gate consumes this; without it differential confirmation cannot // fire even when the probe was written. System.out.println("__NYX_SINK_HIT__"); }} }} "# ); HarnessSource { source, filename: "NyxHarness.java".to_owned(), command: vec![ "java".to_owned(), "-cp".to_owned(), ".".to_owned(), "NyxHarness".to_owned(), ], extra_files: Vec::new(), entry_subpath: None, } } /// Phase 04 — Track J.2 SSTI harness for Java (Thymeleaf). /// /// Reads `NYX_PAYLOAD`, simulates Thymeleaf's `[[${expr}]]` inlined- /// output evaluation, and writes `{"render":""}` plus the /// sink-hit sentinel. Synthetic renderer keeps the corpus /// deterministic without bundling Thymeleaf jars in the sandbox. pub fn emit_ssti_harness(_spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); let source = format!( r#"// Nyx dynamic harness — SSTI Thymeleaf (Phase 04 / Track J.2). // // Routes `NYX_PAYLOAD` through the real `org.thymeleaf.TemplateEngine` // dependency. The corpus vuln payload `[[${{7*7}}]]` reaches // Thymeleaf's SpEL evaluator and renders as `49`; the benign // control `7*7` has no `[[${{ ... }}]]` markers so the engine echoes // it verbatim. // // The companion `pom.xml` (shipped via `HarnessSource::extra_files`) // declares the Thymeleaf dependency; `prepare_java` runs // `mvn dependency:copy-dependencies -DoutputDirectory=lib` against // any workdir that carries a `pom.xml`, then folds `lib/*` into the // javac and runtime classpath via the `-cp` arg below. import java.io.FileWriter; import java.io.IOException; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; public class NyxHarness {{ {shim} static String nyxThymeleafRender(String payload) {{ try {{ TemplateEngine engine = new TemplateEngine(); Context ctx = new Context(); return engine.process(payload, ctx); }} catch (RuntimeException e) {{ return ""; }} }} static void nyxSstiProbe(String rendered) {{ String p = System.getenv("NYX_PROBE_PATH"); if (p == null || p.isEmpty()) return; long now = System.nanoTime(); String pid = System.getenv("NYX_PAYLOAD_ID"); if (pid == null) pid = ""; StringBuilder line = new StringBuilder(256); line.append("{{\"sink_callee\":\"TemplateEngine.process\",\"args\":[{{\"kind\":\"String\",\"value\":\""); nyxJsonEscape(rendered, line); line.append("\"}}],"); line.append("\"captured_at_ns\":").append(now).append(','); line.append("\"payload_id\":\""); nyxJsonEscape(pid, line); line.append("\",\"kind\":{{\"kind\":\"Normal\"}},"); line.append("\"witness\":"); line.append(nyxWitnessJson("TemplateEngine.process", new String[]{{rendered}})); line.append("}}\n"); try (FileWriter fw = new FileWriter(p, true)) {{ fw.write(line.toString()); }} catch (IOException e) {{ // best-effort }} }} public static void main(String[] args) {{ String payload = System.getenv("NYX_PAYLOAD"); if (payload == null) payload = ""; String rendered = nyxThymeleafRender(payload); nyxSstiProbe(rendered); System.out.println("__NYX_SINK_HIT__"); StringBuilder body = new StringBuilder(64); body.append("{{\"render\":\""); nyxJsonEscape(rendered, body); body.append("\"}}"); System.out.println(body.toString()); }} }} "# ); HarnessSource { source, filename: "NyxHarness.java".to_owned(), command: vec![ "java".to_owned(), "-cp".to_owned(), ".:lib/*".to_owned(), "NyxHarness".to_owned(), ], extra_files: vec![("pom.xml".to_owned(), ssti_thymeleaf_pom().to_owned())], entry_subpath: None, } } /// `pom.xml` manifest for the SSTI Thymeleaf harness. /// /// Declares `org.thymeleaf:thymeleaf:3.1.x` so `prepare_java` can resolve /// the runtime classpath via `mvn dependency:copy-dependencies` before /// the javac step. The Thymeleaf 3.1 line is the current LTS branch and /// the lowest Java baseline (`java 11`) we still target across the test /// matrix. fn ssti_thymeleaf_pom() -> &'static str { r#" 4.0.0 com.nyx nyx-harness-thymeleaf 0.0.1 jar 11 11 UTF-8 org.thymeleaf thymeleaf 3.1.2.RELEASE "# } /// Phase 05 — Track J.3 XXE harness for Java (`DocumentBuilderFactory`). /// /// Reads `NYX_PAYLOAD`, parses it with `javax.xml.parsers.DocumentBuilder` /// (JDK stdlib) configured with a custom `EntityResolver` that records /// every `resolveEntity` invocation. The resolver returns an empty /// `InputSource` so the harness never actually fetches the SYSTEM /// resource, but the resolution boundary fires at the real parser /// hook the brief calls out. Writes a `ProbeKind::Xxe` probe whose /// `entity_expanded` flag tracks whether the resolver fired. pub fn emit_xxe_harness(_spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); let source = format!( r#"// Nyx dynamic harness — XXE DocumentBuilderFactory (Phase 05 / Track J.3). import java.io.FileWriter; import java.io.IOException; import java.io.StringReader; import java.net.HttpURLConnection; import java.net.URL; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; public class NyxHarness {{ {shim} static boolean nyxLastExpanded = false; // Build the XML document fed into the parser. Two shapes (Phase 05 // OOB closure, 2026-05-21): // - URL-form NYX_PAYLOAD (`http://...` / `https://...`): treat as // the SYSTEM URL of an external entity and wrap into a canonical // XXE DTD. The entity-resolver hook will perform the loopback // GET so the OOB listener observes the per-finding nonce. // - Anything else: treat as the full XML document (existing shape). static String nyxBuildXxeDocument(String payload) {{ if (payload.startsWith("http://") || payload.startsWith("https://")) {{ String escaped = payload.replace("&", "&").replace("\"", """).replace("<", "<"); return "\n\n]>\n&xxe;"; }} return payload; }} static void nyxXmlParse(String payload) {{ nyxLastExpanded = false; try {{ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); // Mirror the brief's "DocumentBuilderFactory with external // entity resolution enabled" target: leave the factory at // default settings (which historically permit doctype + // external entities) and rely on the EntityResolver hook // to control fetch behaviour. DocumentBuilder db = dbf.newDocumentBuilder(); db.setEntityResolver(new EntityResolver() {{ public InputSource resolveEntity(String publicId, String systemId) {{ // Real parser hook: fired by the SAX/DOM parser for // every `` reference. Mark // expanded. When the SYSTEM URL points at loopback // HTTP, perform a real GET so the OOB listener can // observe the callback (Phase 05 OOB closure). Any // other scheme returns an empty replacement (no fetch). nyxLastExpanded = true; if (systemId != null && (systemId.startsWith("http://127.0.0.1") || systemId.startsWith("http://host-gateway") || systemId.startsWith("http://localhost"))) {{ try {{ HttpURLConnection conn = (HttpURLConnection) new URL(systemId).openConnection(); conn.setConnectTimeout(2000); conn.setReadTimeout(2000); conn.getInputStream().close(); conn.disconnect(); }} catch (Exception ignored) {{ // best-effort OOB fetch }} }} return new InputSource(new StringReader("")); }} }}); try {{ String doc = nyxBuildXxeDocument(payload); db.parse(new InputSource(new StringReader(doc))); }} catch (SAXException | IOException e) {{ // Malformed XML still counts as a parser invocation; // expanded flag reflects whatever the hook saw before // the error. }} }} catch (Exception e) {{ // builder construction failed — leave expanded=false }} }} static void nyxXxeProbe(String payload, boolean expanded) {{ String p = System.getenv("NYX_PROBE_PATH"); if (p == null || p.isEmpty()) return; long now = System.nanoTime(); String pid = System.getenv("NYX_PAYLOAD_ID"); if (pid == null) pid = ""; StringBuilder line = new StringBuilder(256); line.append("{{\"sink_callee\":\"DocumentBuilder.parse\",\"args\":[{{\"kind\":\"String\",\"value\":\""); nyxJsonEscape(payload, line); line.append("\"}}],"); line.append("\"captured_at_ns\":").append(now).append(','); line.append("\"payload_id\":\""); nyxJsonEscape(pid, line); line.append("\",\"kind\":{{\"kind\":\"Xxe\",\"entity_expanded\":").append(expanded ? "true" : "false").append("}},"); line.append("\"witness\":"); line.append(nyxWitnessJson("DocumentBuilder.parse", new String[]{{payload}})); line.append("}}\n"); try (FileWriter fw = new FileWriter(p, true)) {{ fw.write(line.toString()); }} catch (IOException e) {{ // best-effort }} }} public static void main(String[] args) {{ String payload = System.getenv("NYX_PAYLOAD"); if (payload == null) payload = ""; nyxXmlParse(payload); nyxXxeProbe(payload, nyxLastExpanded); System.out.println("__NYX_SINK_HIT__"); StringBuilder body = new StringBuilder(64); body.append("{{\"entity_expanded\":").append(nyxLastExpanded ? "true" : "false").append("}}"); System.out.println(body.toString()); }} }} "# ); HarnessSource { source, filename: "NyxHarness.java".to_owned(), command: vec![ "java".to_owned(), "-cp".to_owned(), ".".to_owned(), "NyxHarness".to_owned(), ], extra_files: Vec::new(), entry_subpath: None, } } /// Phase 06 — Track J.4 LDAP-injection harness for Java /// (`LdapTemplate.search` / `DirContext.search`). /// /// Reads `NYX_PAYLOAD`, splices it into a `(uid=)` filter /// template, and dispatches the resulting filter against the /// in-sandbox LDAP stub via `javax.naming.directory.InitialDirContext` /// over the real LDAPv3 BER wire (the stub's accept loop at /// [`crate::dynamic::stubs::ldap_server::accept_loop`] auto-detects /// the `0x30 SEQUENCE` lead byte and routes through the BER /// reader/writer at [`crate::dynamic::stubs::ldap_ber`]). Falls back /// to an in-process RFC 4515 subset matcher against three canonical /// users (`alice`, `bob`, `carol`) when the env var is unset or JNDI /// bind/search fails, so the harness still produces a verdict on /// hosts that exercise it outside the stub-backed corpus. Writes a /// `ProbeKind::Ldap { entries_returned }` probe whose `n` is the /// count the directory returned. The JNDI provider ships with the /// JDK (`com.sun.jndi.ldap.LdapCtxFactory`) so no extra classpath dep /// is required. pub fn emit_ldap_harness(_spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); let source = format!( r#"// Nyx dynamic harness — LDAP_INJECTION DirContext.search (Phase 06 / Track J.4). import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; public class NyxHarness {{ {shim} static final String[] NYX_LDAP_USERS = new String[] {{ "alice", "bob", "carol" }}; static boolean nyxAttrMatch(String pattern, String uid) {{ if (pattern.equals("*")) return true; int star = pattern.indexOf('*'); if (star < 0) return pattern.equals(uid); String prefix = pattern.substring(0, star); String suffix = pattern.substring(star + 1); return uid.startsWith(prefix) && uid.endsWith(suffix); }} static boolean nyxInnerHasBreak(String inner) {{ int depth = 0; for (int i = 0; i < inner.length(); i++) {{ char c = inner.charAt(i); if (c == '(') depth++; else if (c == ')') {{ depth--; if (depth < 0) return true; }} }} return false; }} /// When `NYX_LDAP_ENDPOINT` is set to `host:port`, route the search /// through the in-sandbox LDAP stub via /// `javax.naming.directory.InitialDirContext` over the real LDAPv3 /// BER wire and return the count of returned entries. Returns /// `-1` when the env var is unset or JNDI fails to bind/search — /// caller falls back to the in-process matcher. static int nyxLdapCountViaJndi(String filter) {{ String ep = System.getenv("NYX_LDAP_ENDPOINT"); if (ep == null || ep.isEmpty()) return -1; Hashtable env = new Hashtable<>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://" + ep + "/"); env.put(Context.SECURITY_AUTHENTICATION, "none"); env.put("com.sun.jndi.ldap.connect.timeout", "2000"); env.put("com.sun.jndi.ldap.read.timeout", "2000"); DirContext ctx = null; try {{ ctx = new InitialDirContext(env); SearchControls controls = new SearchControls(); controls.setSearchScope(SearchControls.SUBTREE_SCOPE); controls.setReturningAttributes(new String[0]); controls.setTimeLimit(2000); NamingEnumeration results = ctx.search("", filter, controls); int count = 0; try {{ while (results.hasMore()) {{ results.next(); count++; }} }} finally {{ try {{ results.close(); }} catch (NamingException ne) {{ /* best-effort */ }} }} return count; }} catch (NamingException ne) {{ return -1; }} finally {{ if (ctx != null) {{ try {{ ctx.close(); }} catch (NamingException ne) {{ /* best-effort */ }} }} }} }} static int nyxLdapCount(String filter) {{ int viaStub = nyxLdapCountViaJndi(filter); if (viaStub >= 0) return viaStub; return nyxLdapCountLocal(filter); }} static int nyxLdapCountLocal(String filter) {{ String f = filter == null ? "" : filter.trim(); if (f.isEmpty()) return 0; if (!f.startsWith("(") || !f.endsWith(")")) return NYX_LDAP_USERS.length; String inner = f.substring(1, f.length() - 1); if (nyxInnerHasBreak(inner)) return NYX_LDAP_USERS.length; if (inner.startsWith("&") || inner.startsWith("|")) {{ List clauses = nyxSplitClauses(inner.substring(1)); int total = 0; for (String u : NYX_LDAP_USERS) {{ boolean ok = inner.startsWith("&"); for (String c : clauses) {{ boolean m = nyxLdapMatch(c, u); ok = inner.startsWith("&") ? (ok && m) : (ok || m); }} if (clauses.isEmpty()) ok = false; if (ok) total++; }} return total; }} int eq = inner.indexOf('='); if (eq < 0) return NYX_LDAP_USERS.length; String attr = inner.substring(0, eq); String pattern = inner.substring(eq + 1); if (!attr.equalsIgnoreCase("uid") && !attr.equalsIgnoreCase("cn")) return NYX_LDAP_USERS.length; int total = 0; for (String u : NYX_LDAP_USERS) {{ if (nyxAttrMatch(pattern, u)) total++; }} return total; }} static boolean nyxLdapMatch(String filter, String uid) {{ return nyxLdapCountLocal(filter) > 0 ? nyxLdapMatchOne(filter, uid) : false; }} static boolean nyxLdapMatchOne(String filter, String uid) {{ String f = filter.trim(); if (!f.startsWith("(") || !f.endsWith(")")) return true; String inner = f.substring(1, f.length() - 1); if (nyxInnerHasBreak(inner)) return true; if (inner.startsWith("&") || inner.startsWith("|")) {{ List clauses = nyxSplitClauses(inner.substring(1)); if (clauses.isEmpty()) return false; boolean ok = inner.startsWith("&"); for (String c : clauses) {{ boolean m = nyxLdapMatchOne(c, uid); ok = inner.startsWith("&") ? (ok && m) : (ok || m); }} return ok; }} int eq = inner.indexOf('='); if (eq < 0) return true; String attr = inner.substring(0, eq); String pattern = inner.substring(eq + 1); if (!attr.equalsIgnoreCase("uid") && !attr.equalsIgnoreCase("cn")) return true; return nyxAttrMatch(pattern, uid); }} static List nyxSplitClauses(String src) {{ List out = new ArrayList<>(); int i = 0; while (i < src.length()) {{ if (src.charAt(i) != '(') {{ i++; continue; }} int depth = 0; int start = i; while (i < src.length()) {{ char c = src.charAt(i); if (c == '(') depth++; else if (c == ')') {{ depth--; if (depth == 0) {{ i++; break; }} }} i++; }} out.add(src.substring(start, i)); }} return out; }} static void nyxLdapProbe(String filter, int entriesReturned) {{ String p = System.getenv("NYX_PROBE_PATH"); if (p == null || p.isEmpty()) return; long now = System.nanoTime(); String pid = System.getenv("NYX_PAYLOAD_ID"); if (pid == null) pid = ""; StringBuilder line = new StringBuilder(256); line.append("{{\"sink_callee\":\"LdapTemplate.search\",\"args\":[{{\"kind\":\"String\",\"value\":\""); nyxJsonEscape(filter, line); line.append("\"}}],"); line.append("\"captured_at_ns\":").append(now).append(','); line.append("\"payload_id\":\""); nyxJsonEscape(pid, line); line.append("\",\"kind\":{{\"kind\":\"Ldap\",\"entries_returned\":").append(entriesReturned).append("}},"); line.append("\"witness\":"); line.append(nyxWitnessJson("LdapTemplate.search", new String[]{{filter}})); line.append("}}\n"); try (FileWriter fw = new FileWriter(p, true)) {{ fw.write(line.toString()); }} catch (IOException e) {{ // best-effort }} }} public static void main(String[] args) {{ String payload = System.getenv("NYX_PAYLOAD"); if (payload == null) payload = ""; String filter = "(uid=" + payload + ")"; int count = nyxLdapCount(filter); nyxLdapProbe(filter, count); System.out.println("__NYX_SINK_HIT__"); StringBuilder body = new StringBuilder(64); body.append("{{\"filter\":\""); nyxJsonEscape(filter, body); body.append("\",\"entries_returned\":").append(count).append("}}"); System.out.println(body.toString()); }} }} "# ); HarnessSource { source, filename: "NyxHarness.java".to_owned(), command: vec![ "java".to_owned(), "-cp".to_owned(), ".".to_owned(), "NyxHarness".to_owned(), ], extra_files: Vec::new(), entry_subpath: None, } } /// Phase 07 — Track J.5 XPath-injection harness for Java /// (`javax.xml.xpath.XPath.evaluate`). /// /// Reads `NYX_PAYLOAD` and (tier (a)) reflectively invokes the entry /// class's static `run(String)` method, which itself calls /// `javax.xml.xpath.XPath.evaluate` against the canonical staged /// document. The harness counts nodes by casting the returned /// `NodeList` and writes a `ProbeKind::Xpath { nodes_returned }` /// probe. When the entry source does not import /// `javax.xml.xpath` (or reflective invocation fails for any reason) /// the harness falls back to the legacy in-process matcher so the /// verdict path stays intact on hosts that exercise the harness /// outside the fixture corpus. pub fn emit_xpath_harness(spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); let corpus_filename = crate::dynamic::stubs::xpath_document::XPATH_CORPUS_FILENAME; let corpus_xml = crate::dynamic::stubs::xpath_document::XPATH_CORPUS_XML; let entry_source = read_entry_source(&spec.entry_file); let entry_class = derive_entry_class(&entry_source); let entry_fqn = derive_entry_qualifier(&entry_source, &entry_class); let entry_method = if spec.entry_name.is_empty() { "run".to_owned() } else { spec.entry_name.clone() }; let source = format!( r#"// Nyx dynamic harness — XPATH_INJECTION javax.xml.xpath.XPath.evaluate (Phase 07 / Track J.5). import java.io.FileWriter; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import org.w3c.dom.NodeList; public class NyxHarness {{ {shim} static void nyxXpathProbe(String expr, int nodesReturned) {{ String p = System.getenv("NYX_PROBE_PATH"); if (p == null || p.isEmpty()) return; long now = System.nanoTime(); String pid = System.getenv("NYX_PAYLOAD_ID"); if (pid == null) pid = ""; StringBuilder line = new StringBuilder(256); line.append("{{\"sink_callee\":\"javax.xml.xpath.XPath.evaluate\",\"args\":[{{\"kind\":\"String\",\"value\":\""); nyxJsonEscape(expr, line); line.append("\"}}],"); line.append("\"captured_at_ns\":").append(now).append(','); line.append("\"payload_id\":\""); nyxJsonEscape(pid, line); line.append("\",\"kind\":{{\"kind\":\"Xpath\",\"nodes_returned\":").append(nodesReturned).append("}},"); line.append("\"witness\":"); line.append(nyxWitnessJson("javax.xml.xpath.XPath.evaluate", new String[]{{expr}})); line.append("}}\n"); try (FileWriter fw = new FileWriter(p, true)) {{ fw.write(line.toString()); }} catch (IOException e) {{ // best-effort }} }} public static void main(String[] args) {{ String payload = System.getenv("NYX_PAYLOAD"); if (payload == null) payload = ""; String expr = "//user[@name='" + payload + "']"; // Phase 07 tier-(a): reflectively invoke the fixture's // `run(String)` so the real `javax.xml.xpath.XPath.evaluate` // call against the staged corpus document runs, then count // the returned `NodeList` nodes. Missing `javax.xml.xpath` // / `org.w3c.dom` on the JDK is the only structural reason // the reflective lookup fails; in that case we emit the // conventional `NYX_IMPORT_ERROR:` stderr marker plus // `System.exit(77)` so the runner maps the outcome to // `RunError::BuildFailed` and the e2e SKIP branch fires. int count; try {{ Class entry = Class.forName("{entry_fqn}"); Method m = entry.getDeclaredMethod("{entry_method}", String.class); m.setAccessible(true); Object result = m.invoke(null, payload); if (result instanceof NodeList) {{ count = ((NodeList) result).getLength(); }} else {{ count = 0; }} }} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {{ System.err.println("NYX_IMPORT_ERROR: " + e.getClass().getName() + ": " + e.getMessage()); System.exit(77); return; }} catch (InvocationTargetException ite) {{ // The fixture itself threw (malformed XPath, parse error, // ...); treat as a 0-node return so a benign fixture that // rejects the payload stays NotConfirmed. count = 0; }} System.out.println("__NYX_XPATH_TIER_A__"); nyxXpathProbe(expr, count); System.out.println("__NYX_SINK_HIT__"); StringBuilder body = new StringBuilder(64); body.append("{{\"expr\":\""); nyxJsonEscape(expr, body); body.append("\",\"nodes_returned\":").append(count).append("}}"); System.out.println(body.toString()); }} }} "# ); let extra_files = vec![(corpus_filename.to_owned(), corpus_xml.to_owned())]; HarnessSource { source, filename: "NyxHarness.java".to_owned(), command: vec![ "java".to_owned(), "-cp".to_owned(), ".".to_owned(), "NyxHarness".to_owned(), ], extra_files, entry_subpath: None, } } /// Phase 08 — Track J.6 header-injection harness for Java /// (`HttpServletResponse.setHeader`). /// /// Reads `NYX_PAYLOAD`, calls a synthetic instrumented /// `response.setHeader("Set-Cookie", value)` shim that records the /// *unmodified* value bytes (including any embedded `\r\n`) via a /// `ProbeKind::HeaderEmit` probe. Mirrors the synthetic-harness /// pattern used by Phase 03 / 04 / 05 / 06 / 07. pub fn emit_header_injection_harness(spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); let extra_files = servlet_stubs_for_entry(&spec.entry_file); let entry_source = read_entry_source(&spec.entry_file); let servlet_pkg = if entry_source.contains("jakarta.servlet") { "jakarta.servlet.http" } else { "javax.servlet.http" }; let entry_class = derive_entry_class(&entry_source); let entry_fqn = derive_entry_qualifier(&entry_source, &entry_class); let entry_method = if spec.entry_name.is_empty() { "run".to_owned() } else { spec.entry_name.clone() }; let has_servlet_stubs = !extra_files.is_empty(); let header_name = "Set-Cookie"; // Tier-(a) path drives the fixture's real `setHeader` call through // the captured-header buffer on the servlet stub. When the entry // file does not import a servlet API the stub is not shipped and // we fall back to the legacy synthetic probe so the harness still // produces a verdict on hosts that do not link the stub. let main_body = if has_servlet_stubs { format!( r#" // Phase 08 tier-(a): instantiate the captured-header response // wrapper, reflectively invoke the fixture's sink call, then // drain every recorded (name, value) pair and emit one // ProbeKind::HeaderEmit per pair so the oracle sees the bytes // the fixture actually passed to setHeader/addHeader. {servlet_pkg}.HttpServletResponse response = new {servlet_pkg}.HttpServletResponse(); boolean fixtureInvoked = false; try {{ Class entry = Class.forName("{entry_fqn}"); Method m = entry.getDeclaredMethod( "{entry_method}", {servlet_pkg}.HttpServletResponse.class, String.class); m.setAccessible(true); m.invoke(null, response, payload); fixtureInvoked = true; }} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {{ // Fixture shape did not match (response, value) — fall // through to the synthetic probe so the verdict path stays // intact for legacy entry shapes. }} catch (InvocationTargetException ite) {{ // The fixture itself threw; treat that as evidence the sink // path was reached and continue to drain captured headers. fixtureInvoked = true; }} java.util.List captured = {servlet_pkg}.HttpServletResponse.nyxDrainHeaders(); if (fixtureInvoked && !captured.isEmpty()) {{ for (String[] pair : captured) {{ nyxHeaderProbe(pair[0], pair[1]); }} }} else {{ // Fixture either rejected the invocation or set no // headers — fall back to the synthetic probe so a benign // fixture that strips CRLF still produces a verdict. nyxHeaderProbe("{header_name}", payload); }}"# ) } else { format!( r#" // No servlet stub available — synthetic probe path. nyxHeaderProbe("{header_name}", payload);"# ) }; let imports = if has_servlet_stubs { "import java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\n" } else { "" }; let source = format!( r#"// Nyx dynamic harness — HEADER_INJECTION HttpServletResponse.setHeader (Phase 08 / Track J.6). import java.io.FileWriter; import java.io.IOException; {imports} public class NyxHarness {{ {shim} static void nyxHeaderProbe(String name, String value) {{ String p = System.getenv("NYX_PROBE_PATH"); if (p == null || p.isEmpty()) return; long now = System.nanoTime(); String pid = System.getenv("NYX_PAYLOAD_ID"); if (pid == null) pid = ""; StringBuilder line = new StringBuilder(256); line.append("{{\"sink_callee\":\"HttpServletResponse.setHeader\",\"args\":["); line.append("{{\"kind\":\"String\",\"value\":\""); nyxJsonEscape(name, line); line.append("\"}},{{\"kind\":\"String\",\"value\":\""); nyxJsonEscape(value, line); line.append("\"}}],"); line.append("\"captured_at_ns\":").append(now).append(','); line.append("\"payload_id\":\""); nyxJsonEscape(pid, line); line.append("\",\"kind\":{{\"kind\":\"HeaderEmit\",\"name\":\""); nyxJsonEscape(name, line); line.append("\",\"value\":\""); nyxJsonEscape(value, line); line.append("\",\"protocol\":\"in-process\"}},"); line.append("\"witness\":"); line.append(nyxWitnessJson("HttpServletResponse.setHeader", new String[]{{name, value}})); line.append("}}\n"); try (FileWriter fw = new FileWriter(p, true)) {{ fw.write(line.toString()); }} catch (IOException e) {{ // best-effort }} }} public static void main(String[] args) {{ String payload = System.getenv("NYX_PAYLOAD"); if (payload == null) payload = ""; {main_body} System.out.println("__NYX_SINK_HIT__"); }} }} "# ); HarnessSource { source, filename: "NyxHarness.java".to_owned(), command: vec![ "java".to_owned(), "-cp".to_owned(), ".".to_owned(), "NyxHarness".to_owned(), ], extra_files, entry_subpath: None, } } /// Phase 09 — Track J.7 open-redirect harness for Java /// (`HttpServletResponse.sendRedirect`). /// /// Reads `NYX_PAYLOAD`, calls a synthetic instrumented /// `response.sendRedirect(value)` shim that records the *unmodified* /// `Location:` value plus the request's origin host via a /// `ProbeKind::Redirect` probe. Mirrors the synthetic-harness /// pattern used by Phase 03 / 04 / 05 / 06 / 07 / 08. pub fn emit_open_redirect_harness(spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); let extra_files = servlet_stubs_for_entry(&spec.entry_file); let entry_source = read_entry_source(&spec.entry_file); let servlet_pkg = if entry_source.contains("jakarta.servlet") { "jakarta.servlet.http" } else { "javax.servlet.http" }; let entry_class = derive_entry_class(&entry_source); let entry_fqn = derive_entry_qualifier(&entry_source, &entry_class); let entry_method = if spec.entry_name.is_empty() { "run".to_owned() } else { spec.entry_name.clone() }; let has_servlet_stubs = !extra_files.is_empty(); // Tier-(a) path drives the fixture's real `sendRedirect` call // through the captured-location field on the servlet stub. Falls // back to the legacy synthetic probe when the entry source does // not import a servlet API so the verdict path stays intact. let main_body = if has_servlet_stubs { format!( r#" // Phase 09 tier-(a): instantiate the captured-redirect response // wrapper, reflectively invoke the fixture's sink call, then // read the captured `Location:` value via getRedirectedUrl() // and emit a single ProbeKind::Redirect probe. {servlet_pkg}.HttpServletResponse response = new {servlet_pkg}.HttpServletResponse(); boolean fixtureInvoked = false; try {{ Class entry = Class.forName("{entry_fqn}"); Method m = entry.getDeclaredMethod( "{entry_method}", {servlet_pkg}.HttpServletResponse.class, String.class); m.setAccessible(true); m.invoke(null, response, payload); fixtureInvoked = true; }} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {{ // Fixture shape did not match (response, value) — fall // through to the synthetic probe. }} catch (InvocationTargetException ite) {{ // Fixture itself threw; the sink path was reached so keep // the captured location if any. fixtureInvoked = true; }} String captured = response.getRedirectedUrl(); if (fixtureInvoked && captured != null) {{ nyxRedirectProbe(captured, requestHost); nyxFollowLocation(captured); }} else {{ nyxRedirectProbe(payload, requestHost); nyxFollowLocation(payload); }}"# ) } else { r#" nyxRedirectProbe(payload, requestHost); nyxFollowLocation(payload);"# .to_owned() }; let imports = if has_servlet_stubs { "import java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\n" } else { "import java.net.HttpURLConnection;\nimport java.net.URL;\n" }; let source = format!( r#"// Nyx dynamic harness — OPEN_REDIRECT HttpServletResponse.sendRedirect (Phase 09 / Track J.7). import java.io.FileWriter; import java.io.IOException; {imports} public class NyxHarness {{ {shim} static void nyxRedirectProbe(String location, String requestHost) {{ String p = System.getenv("NYX_PROBE_PATH"); if (p == null || p.isEmpty()) return; long now = System.nanoTime(); String pid = System.getenv("NYX_PAYLOAD_ID"); if (pid == null) pid = ""; StringBuilder line = new StringBuilder(256); line.append("{{\"sink_callee\":\"HttpServletResponse.sendRedirect\",\"args\":["); line.append("{{\"kind\":\"String\",\"value\":\""); nyxJsonEscape(location, line); line.append("\"}}],"); line.append("\"captured_at_ns\":").append(now).append(','); line.append("\"payload_id\":\""); nyxJsonEscape(pid, line); line.append("\",\"kind\":{{\"kind\":\"Redirect\",\"location\":\""); nyxJsonEscape(location, line); line.append("\",\"request_host\":\""); nyxJsonEscape(requestHost, line); line.append("\"}},"); line.append("\"witness\":"); line.append(nyxWitnessJson("HttpServletResponse.sendRedirect", new String[]{{location}})); line.append("}}\n"); try (FileWriter fw = new FileWriter(p, true)) {{ fw.write(line.toString()); }} catch (IOException e) {{ // best-effort }} }} // Phase 09 OOB closure: when the captured Location is a fully-qualified // loopback URL, follow it with a real GET so the OOB listener records // the per-finding nonce. Skips non-loopback hosts (no real network egress) // and any non-HTTP scheme. Best-effort: failures do not propagate, the // listener may still have observed the connect before the read errored. static void nyxFollowLocation(String location) {{ if (location == null || location.isEmpty()) return; String lower = location.toLowerCase(); if (!(lower.startsWith("http://127.0.0.1") || lower.startsWith("http://localhost") || lower.startsWith("http://host-gateway"))) {{ return; }} try {{ HttpURLConnection conn = (HttpURLConnection) new URL(location).openConnection(); conn.setConnectTimeout(2000); conn.setReadTimeout(2000); conn.setInstanceFollowRedirects(false); conn.getInputStream().close(); conn.disconnect(); }} catch (Exception ignored) {{ // best-effort OOB fetch }} }} public static void main(String[] args) {{ String payload = System.getenv("NYX_PAYLOAD"); if (payload == null) payload = ""; String requestHost = "example.com"; {main_body} System.out.println("__NYX_SINK_HIT__"); }} }} "# ); HarnessSource { source, filename: "NyxHarness.java".to_owned(), command: vec![ "java".to_owned(), "-cp".to_owned(), ".".to_owned(), "NyxHarness".to_owned(), ], extra_files, entry_subpath: None, } } /// Stage the `javax.servlet.*` / `jakarta.servlet.*` stub bundle when /// the entry source imports either namespace. Phase 08 / 09 fixtures /// (`HttpServletResponse.setHeader` / `.sendRedirect`) carry the /// `import javax.servlet.http.HttpServletResponse;` so `javac` over /// the workdir's `*.java` set needs the symbols on the classpath even /// though `NyxHarness.java` itself uses no servlet types. Without the /// stubs the verifier flips to `BuildFailed` and the per-lang e2e /// tests silently skip via the SKIP-on-`BuildFailed` branch. fn servlet_stubs_for_entry(entry_file: &str) -> Vec<(String, String)> { let entry_source = read_entry_source(entry_file); if entry_source.contains("javax.servlet") || entry_source.contains("jakarta.servlet") { crate::dynamic::lang::java_servlet_stubs::servlet_stub_files() } else { Vec::new() } } /// Public wrapper to detect the shape for a finalised `HarnessSpec`, /// reading the entry file from disk. Exposed so test helpers can pin a /// per-fixture shape without round-tripping through [`emit`]. pub fn detect_shape(spec: &HarnessSpec) -> JavaShape { let entry_source = read_entry_source(&spec.entry_file); JavaShape::detect(spec, &entry_source) } fn read_entry_source(entry_file: &str) -> String { let candidates = [ PathBuf::from(entry_file), PathBuf::from(".").join(entry_file), ]; for path in &candidates { if let Ok(s) = std::fs::read_to_string(path) { return s; } } String::new() } /// Locate the harness's target class by parsing the entry source for a /// `public class X` (or `public final class X` / `public abstract class /// X`) declaration. Falls back to `"Entry"` when the source is empty /// or no public-class line is present. /// /// The returned name drives both the in-harness invocation /// (`{class}.method(...)` / `Class.forName(class)`) and the /// `entry_subpath` (`{class}.java`) so javac's filename-vs-public-class /// invariant holds for both the legacy `public class Entry` fixtures /// and the Phase 14 shape fixtures that ship `public class Vuln` /// (or `public class Benign`). fn derive_entry_class(source: &str) -> String { parse_public_class_name(source).unwrap_or_else(|| "Entry".to_owned()) } /// Resolve the entry class as a fully-qualified Java name when the /// entry source declares a `package`. Falls back to the bare simple /// name when the source has no package declaration (the legacy /// default-package fixture path). /// /// OWASP Benchmark testcases ship with `package /// org.owasp.benchmark.testcode;` headers; javac compiles their /// sources into `org/owasp/benchmark/testcode/.class` under /// the workdir, so `NyxHarness` (which itself lives in the default /// package) cannot resolve them via the simple name alone. Using /// the FQN in the harness's `Class.forName` / `.class` references /// keeps both default-package and packaged entries linkable. fn derive_entry_qualifier(source: &str, simple_name: &str) -> String { match parse_package_name(source) { Some(pkg) => format!("{pkg}.{simple_name}"), None => simple_name.to_owned(), } } fn parse_package_name(source: &str) -> Option { for line in source.lines() { let trimmed = line.trim_start(); let rest = match trimmed.strip_prefix("package ") { Some(r) => r, None => continue, }; let end = rest.find(';')?; let name = rest[..end].trim(); if !name.is_empty() && name .chars() .all(|c| c.is_alphanumeric() || c == '_' || c == '.') { return Some(name.to_owned()); } return None; } None } fn parse_public_class_name(source: &str) -> Option { for line in source.lines() { let l = line.trim_start(); let rest = match l .strip_prefix("public class ") .or_else(|| l.strip_prefix("public final class ")) .or_else(|| l.strip_prefix("public abstract class ")) { Some(r) => r, None => continue, }; let name: String = rest .chars() .take_while(|c| c.is_alphanumeric() || *c == '_' || *c == '$') .collect(); if !name.is_empty() { return Some(name); } } None } // ── Per-shape harness generation ──────────────────────────────────────────── fn generate_harness_java(spec: &HarnessSpec, shape: JavaShape, entry_class: &str) -> String { let probe = probe_shim(); let pre_call = pre_call_setup(spec); let invocation = invoke_for_shape(spec, shape, entry_class); let helpers = shape_helpers(shape); // Reflection-driven shapes throw `InvocationTargetException` on // user-code failure; non-reflection shapes (`StaticMethod`, // `StaticMain`) call the entry directly and would surface an // "unreachable catch" javac error if the specific catch clause is // kept. Emit only the broad `Throwable` catch for those shapes. let extra_catch = if shape_uses_reflection(shape) { r#" } catch (InvocationTargetException ite) { Throwable cause = ite.getCause() == null ? ite : ite.getCause(); System.err.println("NYX_EXCEPTION: " + cause.getClass().getName() + ": " + cause.getMessage()); "# } else { "" }; // Reflection imports are only used by shapes whose helpers / catch // clause reference them; emitting them for `StaticMethod` / // `StaticMain` produces unused-import warnings under javac -Xlint. let imports = if shape_uses_reflection(shape) { "import java.lang.reflect.Method;\nimport java.lang.reflect.Constructor;\nimport java.lang.reflect.InvocationTargetException;\n\n" } else { "" }; format!( r#"// Nyx dynamic harness — auto-generated, do not edit (Phase 14 — JavaShape::{shape:?}). {imports}public class NyxHarness {{ {probe} {helpers} public static void main(String[] args) {{ String payload = nyxPayload(); {pre_call} try {{ {invocation} {extra_catch}}} catch (Throwable e) {{ System.err.println("NYX_EXCEPTION: " + e.getClass().getName() + ": " + e.getMessage()); }} }} static String nyxPayload() {{ String v = System.getenv("NYX_PAYLOAD"); if (v != null && !v.isEmpty()) {{ return v; }} String b64 = System.getenv("NYX_PAYLOAD_B64"); if (b64 != null && !b64.isEmpty()) {{ byte[] decoded = java.util.Base64.getDecoder().decode(b64); return new String(decoded, java.nio.charset.StandardCharsets.UTF_8); }} return ""; }} }} "#, shape = shape, imports = imports, probe = probe, helpers = helpers, pre_call = pre_call, invocation = invocation, ) } fn pre_call_setup(spec: &HarnessSpec) -> String { match &spec.payload_slot { PayloadSlot::EnvVar(name) => { format!(" System.setProperty({name:?}, payload);\n") } _ => String::new(), } } /// Emit the per-shape entry-invocation block. Shapes that need /// reflection plumbing rely on helpers from [`shape_helpers`]. fn invoke_for_shape(spec: &HarnessSpec, shape: JavaShape, entry_class: &str) -> String { let method = spec.entry_name.as_str(); match shape { JavaShape::StaticMethod => format!(" {entry_class}.{method}(payload);"), JavaShape::StaticMain => format!( " String[] mainArgs = new String[] {{ payload }};\n {entry_class}.main(mainArgs);" ), JavaShape::ServletDoGet => { format!(" invokeServlet({entry_class}.class, \"doGet\", payload, \"GET\");") } JavaShape::ServletDoPost => format!( " invokeServlet({entry_class}.class, \"doPost\", payload, \"POST\");" ), JavaShape::SpringController => { if spec.java_toolchain.with_spring_test { // Phase 14 (Track L.12) — `with_spring_test`-enabled // Spring shape: the v1 implementation still drives the // reflective path because the synthetic harness does // not bundle SpringBoot test deps. The flag flips a // marker on stdout so the verifier can confirm the // toolchain knob propagated. format!( " System.out.println(\"NYX_SPRING_TEST=1\");\n invokeReflective({entry_class}.class, \"{method}\", payload);" ) } else { format!(" invokeReflective({entry_class}.class, \"{method}\", payload);") } } JavaShape::QuarkusRoute => { format!(" invokeReflective({entry_class}.class, \"{method}\", payload);") } JavaShape::MicronautRoute => { format!(" invokeReflective({entry_class}.class, \"{method}\", payload);") } JavaShape::JunitTest => { format!(" invokeJunitTest({entry_class}.class, \"{method}\");") } } } /// Per-shape helper methods spliced into the harness class. fn shape_helpers(shape: JavaShape) -> &'static str { match shape { JavaShape::StaticMethod | JavaShape::StaticMain => "", JavaShape::ServletDoGet | JavaShape::ServletDoPost => SERVLET_HELPER, JavaShape::SpringController | JavaShape::QuarkusRoute | JavaShape::MicronautRoute => { REFLECTIVE_HELPER } JavaShape::JunitTest => JUNIT_HELPER, } } fn shape_uses_reflection(shape: JavaShape) -> bool { !matches!(shape, JavaShape::StaticMethod | JavaShape::StaticMain) } /// Reflective servlet invocation. Walks `cls`'s declared methods for a /// match on `methodName` and invokes with `(StubReq, StubResp)`. When /// the fixture's `doGet`/`doPost` takes only a `String` payload (the /// stub-free path used by many fixtures), the helper falls back to /// `invokeReflective`. const SERVLET_HELPER: &str = r#" static void invokeServlet(Class cls, String methodName, String payload, String httpMethod) throws Exception { Method match = null; for (Method m : cls.getDeclaredMethods()) { if (!m.getName().equals(methodName)) continue; match = m; break; } if (match == null) { throw new NoSuchMethodException(cls.getName() + "." + methodName); } match.setAccessible(true); Object instance = null; if (!java.lang.reflect.Modifier.isStatic(match.getModifiers())) { instance = newDefaultInstance(cls); } Class[] params = match.getParameterTypes(); Object[] args = new Object[params.length]; for (int i = 0; i < params.length; i++) { Class p = params[i]; if (p.equals(String.class)) { args[i] = payload; } else if (p.getName().endsWith("HttpServletRequest")) { args[i] = buildRequestStub(p, payload, httpMethod); } else if (p.getName().endsWith("HttpServletResponse")) { args[i] = buildResponseStub(p); } else { args[i] = null; } } match.invoke(instance, args); } static Object newDefaultInstance(Class cls) throws Exception { Constructor ctor = cls.getDeclaredConstructor(); ctor.setAccessible(true); return ctor.newInstance(); } static Object buildRequestStub(Class reqType, String payload, String method) throws Exception { // Best-effort: invoke a no-arg constructor and call any // `setParameter`/`setMethod` setters the stub exposes. When // the type cannot be instantiated, fall back to null and let // the fixture handle the missing parameter. try { Constructor ctor = reqType.getDeclaredConstructor(); ctor.setAccessible(true); Object stub = ctor.newInstance(); try { Method setParam = reqType.getMethod("setParameter", String.class, String.class); setParam.invoke(stub, "payload", payload); } catch (NoSuchMethodException ignore) {} try { Method setMethod = reqType.getMethod("setMethod", String.class); setMethod.invoke(stub, method); } catch (NoSuchMethodException ignore) {} try { Method setBody = reqType.getMethod("setBody", String.class); setBody.invoke(stub, payload); } catch (NoSuchMethodException ignore) {} return stub; } catch (NoSuchMethodException e) { return null; } } static Object buildResponseStub(Class respType) throws Exception { try { Constructor ctor = respType.getDeclaredConstructor(); ctor.setAccessible(true); return ctor.newInstance(); } catch (NoSuchMethodException e) { return null; } } static void invokeReflective(Class cls, String methodName, String payload) throws Exception { Method match = null; for (Method m : cls.getDeclaredMethods()) { if (m.getName().equals(methodName)) { match = m; break; } } if (match == null) { throw new NoSuchMethodException(cls.getName() + "." + methodName); } match.setAccessible(true); Object instance = null; if (!java.lang.reflect.Modifier.isStatic(match.getModifiers())) { instance = newDefaultInstance(cls); } Class[] params = match.getParameterTypes(); Object[] args = new Object[params.length]; for (int i = 0; i < params.length; i++) { args[i] = params[i].equals(String.class) ? payload : null; } match.invoke(instance, args); } "#; /// Reflective Spring / Quarkus invocation. Same shape as the servlet /// reflective fallback but routed through a dedicated helper for /// clarity in the generated harness. const REFLECTIVE_HELPER: &str = r#" static Object newDefaultInstance(Class cls) throws Exception { Constructor ctor = cls.getDeclaredConstructor(); ctor.setAccessible(true); return ctor.newInstance(); } static void invokeReflective(Class cls, String methodName, String payload) throws Exception { Method match = null; for (Method m : cls.getDeclaredMethods()) { if (m.getName().equals(methodName)) { match = m; break; } } if (match == null) { throw new NoSuchMethodException(cls.getName() + "." + methodName); } match.setAccessible(true); Object instance = null; if (!java.lang.reflect.Modifier.isStatic(match.getModifiers())) { instance = newDefaultInstance(cls); } Class[] params = match.getParameterTypes(); Object[] args = new Object[params.length]; for (int i = 0; i < params.length; i++) { args[i] = params[i].equals(String.class) ? payload : null; } match.invoke(instance, args); } "#; /// Phase 19 (Track M.1) — class-method harness for Java. /// /// Emits a `NyxHarness.java` whose `main` reflectively constructs the /// target class via its no-arg constructor (when available) — or /// fills primitive parameters with defaults + object parameters with /// the Phase 19 [`crate::dynamic::stubs::MockKind`] doubles when the /// no-arg path is missing — and invokes `method(payload)`. The class /// is loaded via the same FQN qualifier used by the regular Java /// shapes so it works on both default-package fixtures and packaged /// OWASP-style entries. fn emit_class_method_harness( spec: &HarnessSpec, class: &str, method: &str, entry_class: &str, ) -> HarnessSource { let probe = probe_shim(); let pre_call = pre_call_setup(spec); let mock_http = crate::dynamic::stubs::mock_source( crate::dynamic::stubs::MockKind::HttpClient, crate::symbol::Lang::Java, ); let mock_db = crate::dynamic::stubs::mock_source( crate::dynamic::stubs::MockKind::DatabaseConnection, crate::symbol::Lang::Java, ); let mock_log = crate::dynamic::stubs::mock_source( crate::dynamic::stubs::MockKind::Logger, crate::symbol::Lang::Java, ); let source = format!( r#"// Nyx dynamic harness — class method (Phase 19 / Track M.1). import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.InvocationTargetException; public class NyxHarness {{ {probe} {mock_http} {mock_db} {mock_log} static Object nyxBuildReceiver(Class cls) throws Exception {{ // Preferred path: zero-arg ctor. try {{ Constructor c = cls.getDeclaredConstructor(); c.setAccessible(true); return c.newInstance(); }} catch (NoSuchMethodException ignore) {{ }} // Fallback path: walk declared ctors and stub each formal. for (Constructor c : cls.getDeclaredConstructors()) {{ c.setAccessible(true); Class[] params = c.getParameterTypes(); Object[] args = new Object[params.length]; for (int i = 0; i < params.length; i++) {{ args[i] = nyxStubForType(params[i]); }} try {{ return c.newInstance(args); }} catch (Exception ignore) {{}} }} return null; }} static Object nyxStubForType(Class t) {{ String n = t.getName().toLowerCase(); if (n.contains("http") || n.contains("client")) return new MockHttpClient(); if (n.contains("database") || n.contains("connection") || n.contains("session") || n.contains("repository")) return new MockDatabaseConnection(); if (n.contains("logger") || n.contains("log")) return new MockLogger(); if (t.equals(String.class)) return ""; if (t.equals(int.class) || t.equals(Integer.class)) return 0; if (t.equals(long.class) || t.equals(Long.class)) return 0L; if (t.equals(boolean.class) || t.equals(Boolean.class)) return false; return null; }} public static void main(String[] args) {{ String payload = nyxPayload(); {pre_call} try {{ Class cls; try {{ cls = Class.forName({class_fqn:?}); }} catch (ClassNotFoundException cnfe) {{ cls = Class.forName({entry_class_fqn:?}); }} Object instance = nyxBuildReceiver(cls); if (instance == null) {{ System.err.println("NYX_CLASS_CTOR_FAILED: " + cls.getName()); System.exit(78); }} Method match = null; for (Method m : cls.getDeclaredMethods()) {{ if (m.getName().equals({method:?})) {{ match = m; break; }} }} if (match == null) {{ System.err.println("NYX_METHOD_NOT_FOUND: " + {method:?}); System.exit(78); }} match.setAccessible(true); Class[] params = match.getParameterTypes(); Object[] mArgs = new Object[params.length]; for (int i = 0; i < params.length; i++) {{ mArgs[i] = params[i].equals(String.class) ? payload : nyxStubForType(params[i]); }} match.invoke(instance, mArgs); }} catch (InvocationTargetException ite) {{ Throwable cause = ite.getCause() == null ? ite : ite.getCause(); System.err.println("NYX_EXCEPTION: " + cause.getClass().getName() + ": " + cause.getMessage()); }} catch (Throwable e) {{ System.err.println("NYX_EXCEPTION: " + e.getClass().getName() + ": " + e.getMessage()); }} }} static String nyxPayload() {{ String v = System.getenv("NYX_PAYLOAD"); if (v != null && !v.isEmpty()) {{ return v; }} String b64 = System.getenv("NYX_PAYLOAD_B64"); if (b64 != null && !b64.isEmpty()) {{ byte[] decoded = java.util.Base64.getDecoder().decode(b64); return new String(decoded, java.nio.charset.StandardCharsets.UTF_8); }} return ""; }} }} "#, class_fqn = class, entry_class_fqn = entry_class, method = method, pre_call = pre_call, ); HarnessSource { source, filename: "NyxHarness.java".to_owned(), command: vec![ "java".to_owned(), "-cp".to_owned(), ".".to_owned(), "NyxHarness".to_owned(), ], extra_files: vec![], entry_subpath: Some(format!("{entry_class}.java")), } } /// Phase 20 (Track M.2) — message-handler harness for Java. /// /// Locates `entry_class` (the fixture's public class) reflectively, /// instantiates it via its no-arg ctor (or via the stubbed-dependency /// fallback path used by [`emit_class_method_harness`]), mounts the /// broker loopback selected by `spec.framework.adapter` /// (`kafka-java` → `NyxKafkaLoopback`, `sqs-java` → `NyxSqsLoopback`, /// `rabbit-java` → `NyxRabbitChannel`; default → Kafka), subscribes the /// handler method named by `spec.entry_name`, and publishes the payload /// onto `queue`. fn emit_message_handler_harness( spec: &HarnessSpec, queue: &str, entry_class: &str, ) -> HarnessSource { let probe = probe_shim(); let handler = &spec.entry_name; let broker = java_broker_for_adapter(spec); let kafka_src = crate::dynamic::stubs::kafka_source(crate::symbol::Lang::Java); let sqs_src = crate::dynamic::stubs::sqs_source(crate::symbol::Lang::Java); let rabbit_src = crate::dynamic::stubs::rabbit_source(crate::symbol::Lang::Java); let (publish_marker, dispatch_block) = match broker { JavaBroker::Sqs => ( crate::dynamic::stubs::SQS_PUBLISH_MARKER, format!( r#" NyxSqsLoopback brokerRef = new NyxSqsLoopback(); brokerRef.subscribe({queue:?}, env -> {{ System.out.println("__NYX_SINK_HIT__"); try {{ java.lang.reflect.Method m = entryInst.getClass().getDeclaredMethod({handler:?}, java.util.Map.class); m.setAccessible(true); m.invoke(entryInst, env); }} catch (Exception e) {{ Throwable c = (e instanceof java.lang.reflect.InvocationTargetException && e.getCause() != null) ? e.getCause() : e; System.err.println("NYX_EXCEPTION: " + c.getClass().getName() + ": " + c.getMessage()); }} }}); System.out.println({publish_marker:?} + " " + {queue:?}); brokerRef.publish({queue:?}, payload);"#, handler = handler, queue = queue, publish_marker = crate::dynamic::stubs::SQS_PUBLISH_MARKER, ), ), JavaBroker::Rabbit => ( crate::dynamic::stubs::RABBIT_PUBLISH_MARKER, format!( r#" NyxRabbitChannel chan = new NyxRabbitChannel(); chan.basicConsume({queue:?}, (mid, body) -> {{ System.out.println("__NYX_SINK_HIT__"); try {{ java.lang.reflect.Method m = entryInst.getClass().getDeclaredMethod({handler:?}, String.class, String.class); m.setAccessible(true); m.invoke(entryInst, mid, body); }} catch (NoSuchMethodException nsme) {{ try {{ java.lang.reflect.Method m2 = entryInst.getClass().getDeclaredMethod({handler:?}, String.class); m2.setAccessible(true); m2.invoke(entryInst, body); }} catch (Exception ie) {{ Throwable c = (ie instanceof java.lang.reflect.InvocationTargetException && ie.getCause() != null) ? ie.getCause() : ie; System.err.println("NYX_EXCEPTION: " + c.getClass().getName() + ": " + c.getMessage()); }} }} catch (Exception e) {{ Throwable c = (e instanceof java.lang.reflect.InvocationTargetException && e.getCause() != null) ? e.getCause() : e; System.err.println("NYX_EXCEPTION: " + c.getClass().getName() + ": " + c.getMessage()); }} }}); System.out.println({publish_marker:?} + " " + {queue:?}); chan.basicPublish("", {queue:?}, payload);"#, handler = handler, queue = queue, publish_marker = crate::dynamic::stubs::RABBIT_PUBLISH_MARKER, ), ), JavaBroker::Kafka => ( crate::dynamic::stubs::KAFKA_PUBLISH_MARKER, format!( r#" NyxKafkaLoopback brokerRef = new NyxKafkaLoopback(); brokerRef.subscribe({queue:?}, body -> {{ System.out.println("__NYX_SINK_HIT__"); try {{ java.lang.reflect.Method m = entryInst.getClass().getDeclaredMethod({handler:?}, String.class); m.setAccessible(true); m.invoke(entryInst, body); }} catch (Exception e) {{ Throwable c = (e instanceof java.lang.reflect.InvocationTargetException && e.getCause() != null) ? e.getCause() : e; System.err.println("NYX_EXCEPTION: " + c.getClass().getName() + ": " + c.getMessage()); }} }}); System.out.println({publish_marker:?} + " " + {queue:?}); brokerRef.publish({queue:?}, payload);"#, handler = handler, queue = queue, publish_marker = crate::dynamic::stubs::KAFKA_PUBLISH_MARKER, ), ), }; let _ = publish_marker; let source = format!( r#"// Nyx dynamic harness — message handler (Phase 20 / Track M.2). import java.lang.reflect.Constructor; import java.lang.reflect.Method; public class NyxHarness {{ {probe} {kafka_src} {sqs_src} {rabbit_src} public static void main(String[] args) {{ String payload = nyxPayload(); try {{ Class entryCls = Class.forName({entry_class:?}); Constructor ctor = entryCls.getDeclaredConstructor(); ctor.setAccessible(true); final Object entryInst = ctor.newInstance(); {dispatch_block} }} catch (Throwable e) {{ System.err.println("NYX_EXCEPTION: " + e.getClass().getName() + ": " + e.getMessage()); }} }} static String nyxPayload() {{ String v = System.getenv("NYX_PAYLOAD"); if (v != null && !v.isEmpty()) return v; String b64 = System.getenv("NYX_PAYLOAD_B64"); if (b64 != null && !b64.isEmpty()) {{ byte[] decoded = java.util.Base64.getDecoder().decode(b64); return new String(decoded, java.nio.charset.StandardCharsets.UTF_8); }} return ""; }} }} "#, entry_class = entry_class, dispatch_block = dispatch_block, ); HarnessSource { source, filename: "NyxHarness.java".to_owned(), command: vec![ "java".to_owned(), "-cp".to_owned(), ".".to_owned(), "NyxHarness".to_owned(), ], extra_files: vec![], entry_subpath: Some(format!("{entry_class}.java")), } } // ── Phase 21 (Track M.3) — synthetic entry-kind harnesses ───────────────────── fn emit_scheduled_job_harness( spec: &HarnessSpec, schedule: Option<&str>, entry_class: &str, ) -> HarnessSource { let probe = probe_shim(); let pre_call = pre_call_setup(spec); let method = &spec.entry_name; let schedule_repr = schedule.unwrap_or(""); let source = format!( r#"// Nyx dynamic harness — scheduled job (Phase 21 / Track M.3). import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.InvocationTargetException; public class NyxHarness {{ {probe} public static void main(String[] args) {{ String payload = nyxPayload(); {pre_call} System.out.println("__NYX_SCHEDULED_JOB__: " + {schedule:?}); System.out.println("__NYX_SINK_HIT__"); try {{ Class cls = Class.forName({entry_class:?}); Constructor ctor = cls.getDeclaredConstructor(); ctor.setAccessible(true); Object instance = ctor.newInstance(); Method m = null; for (Method candidate : cls.getDeclaredMethods()) {{ if (candidate.getName().equals({method:?})) {{ m = candidate; break; }} }} if (m == null) {{ System.err.println("NYX_METHOD_NOT_FOUND: " + {method:?}); System.exit(78); }} m.setAccessible(true); Class[] params = m.getParameterTypes(); Object[] mArgs = new Object[params.length]; for (int i = 0; i < params.length; i++) {{ mArgs[i] = params[i].equals(String.class) ? payload : null; }} m.invoke(instance, mArgs); }} catch (InvocationTargetException ite) {{ Throwable cause = ite.getCause() == null ? ite : ite.getCause(); System.err.println("NYX_EXCEPTION: " + cause.getClass().getName() + ": " + cause.getMessage()); }} catch (Throwable e) {{ System.err.println("NYX_EXCEPTION: " + e.getClass().getName() + ": " + e.getMessage()); }} }} static String nyxPayload() {{ String v = System.getenv("NYX_PAYLOAD"); if (v != null && !v.isEmpty()) return v; String b64 = System.getenv("NYX_PAYLOAD_B64"); if (b64 != null && !b64.isEmpty()) {{ byte[] decoded = java.util.Base64.getDecoder().decode(b64); return new String(decoded, java.nio.charset.StandardCharsets.UTF_8); }} return ""; }} }} "#, entry_class = entry_class, method = method, schedule = schedule_repr, pre_call = pre_call, ); HarnessSource { source, filename: "NyxHarness.java".to_owned(), command: vec![ "java".to_owned(), "-cp".to_owned(), ".".to_owned(), "NyxHarness".to_owned(), ], extra_files: vec![], entry_subpath: Some(format!("{entry_class}.java")), } } fn emit_middleware_harness(spec: &HarnessSpec, name: &str, entry_class: &str) -> HarnessSource { let probe = probe_shim(); let pre_call = pre_call_setup(spec); let method = &spec.entry_name; let source = format!( r#"// Nyx dynamic harness — middleware (Phase 21 / Track M.3). import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.InvocationTargetException; public class NyxHarness {{ {probe} public static void main(String[] args) {{ String payload = nyxPayload(); {pre_call} System.out.println("__NYX_MIDDLEWARE__: " + {name:?}); System.out.println("__NYX_SINK_HIT__"); try {{ Class cls = Class.forName({entry_class:?}); Constructor ctor = cls.getDeclaredConstructor(); ctor.setAccessible(true); Object instance = ctor.newInstance(); Method m = null; for (Method candidate : cls.getDeclaredMethods()) {{ if (candidate.getName().equals({method:?})) {{ m = candidate; break; }} }} if (m == null) {{ System.err.println("NYX_METHOD_NOT_FOUND: " + {method:?}); System.exit(78); }} m.setAccessible(true); Class[] params = m.getParameterTypes(); Object[] mArgs = new Object[params.length]; for (int i = 0; i < params.length; i++) {{ mArgs[i] = params[i].equals(String.class) ? payload : null; }} m.invoke(instance, mArgs); }} catch (InvocationTargetException ite) {{ Throwable cause = ite.getCause() == null ? ite : ite.getCause(); System.err.println("NYX_EXCEPTION: " + cause.getClass().getName() + ": " + cause.getMessage()); }} catch (Throwable e) {{ System.err.println("NYX_EXCEPTION: " + e.getClass().getName() + ": " + e.getMessage()); }} }} static String nyxPayload() {{ String v = System.getenv("NYX_PAYLOAD"); if (v != null && !v.isEmpty()) return v; String b64 = System.getenv("NYX_PAYLOAD_B64"); if (b64 != null && !b64.isEmpty()) {{ byte[] decoded = java.util.Base64.getDecoder().decode(b64); return new String(decoded, java.nio.charset.StandardCharsets.UTF_8); }} return ""; }} }} "#, entry_class = entry_class, method = method, name = name, pre_call = pre_call, ); HarnessSource { source, filename: "NyxHarness.java".to_owned(), command: vec![ "java".to_owned(), "-cp".to_owned(), ".".to_owned(), "NyxHarness".to_owned(), ], extra_files: vec![], entry_subpath: Some(format!("{entry_class}.java")), } } #[derive(Debug, Clone, Copy)] enum JavaBroker { Kafka, Sqs, Rabbit, } fn java_broker_for_adapter(spec: &HarnessSpec) -> JavaBroker { let adapter = spec .framework .as_ref() .map(|b| b.adapter.as_str()) .unwrap_or(""); match adapter { "sqs-java" => JavaBroker::Sqs, "rabbit-java" => JavaBroker::Rabbit, _ => JavaBroker::Kafka, } } /// Reflective JUnit-shape invocation. Reads the payload from /// `NYX_PAYLOAD` (no method argument) — JUnit tests typically capture /// inputs through fields or `System.getenv`. const JUNIT_HELPER: &str = r#" static Object newDefaultInstance(Class cls) throws Exception { Constructor ctor = cls.getDeclaredConstructor(); ctor.setAccessible(true); return ctor.newInstance(); } static void invokeJunitTest(Class cls, String methodName) throws Exception { Method match = null; for (Method m : cls.getDeclaredMethods()) { if (m.getName().equals(methodName)) { match = m; break; } } if (match == null) { throw new NoSuchMethodException(cls.getName() + "." + methodName); } match.setAccessible(true); Object instance = null; if (!java.lang.reflect.Modifier.isStatic(match.getModifiers())) { instance = newDefaultInstance(cls); } match.invoke(instance); } "#; #[cfg(test)] mod tests { use super::*; use crate::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot}; use crate::labels::Cap; use crate::symbol::Lang; fn make_spec(payload_slot: PayloadSlot) -> HarnessSpec { HarnessSpec { finding_id: "java00000000001".into(), entry_file: "src/main/java/App.java".into(), entry_name: "processInput".into(), entry_kind: EntryKind::Function, lang: Lang::Java, toolchain_id: "java-21".into(), payload_slot, expected_cap: Cap::SQL_QUERY, constraint_hints: vec![], sink_file: "src/main/java/App.java".into(), sink_line: 25, spec_hash: "java00000000001".into(), derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps, stubs_required: vec![], framework: None, java_toolchain: crate::dynamic::spec::JavaToolchain::default(), } } #[test] fn emit_produces_source() { let spec = make_spec(PayloadSlot::Param(0)); let harness = emit(&spec).unwrap(); assert!(harness.source.contains("public class NyxHarness")); assert!(harness.source.contains("nyxPayload()")); assert!(harness.source.contains("Entry.processInput(payload)")); assert_eq!(harness.filename, "NyxHarness.java"); assert_eq!(harness.command, vec!["java", "-cp", ".", "NyxHarness"]); } #[test] fn emit_entry_subpath_default_static_method_is_entry_java() { let spec = make_spec(PayloadSlot::Param(0)); let harness = emit(&spec).unwrap(); assert_eq!(harness.entry_subpath, Some("Entry.java".to_owned())); } #[test] fn emit_env_var_slot() { let spec = make_spec(PayloadSlot::EnvVar("DB_PASSWORD".into())); let harness = emit(&spec).unwrap(); assert!(harness.source.contains("System.setProperty")); assert!(harness.source.contains("\"DB_PASSWORD\"")); } #[test] fn emit_param_gt_0_is_accepted_for_static_method() { // Phase 14: PayloadSlot::Param(n>0) is no longer rejected; the // emitter routes the payload via the first-arg slot regardless // (the runner has already pinned the slot at spec time). let spec = make_spec(PayloadSlot::Param(1)); let harness = emit(&spec).unwrap(); assert!(harness.source.contains("processInput(payload)")); } #[test] fn emit_stdin_is_unsupported() { let spec = make_spec(PayloadSlot::Stdin); let err = emit(&spec).unwrap_err(); assert_eq!(err, UnsupportedReason::PayloadSlotUnsupported); } #[test] fn entry_kinds_supported_is_non_empty() { assert!(!JavaEmitter.entry_kinds_supported().is_empty()); assert!( JavaEmitter .entry_kinds_supported() .contains(&EntryKindTag::Function) ); assert!( JavaEmitter .entry_kinds_supported() .contains(&EntryKindTag::HttpRoute) ); assert!( JavaEmitter .entry_kinds_supported() .contains(&EntryKindTag::CliSubcommand) ); } #[test] fn entry_kind_hint_names_attempted_and_phase() { let hint = JavaEmitter.entry_kind_hint(EntryKindTag::LibraryApi); assert!(hint.contains("LibraryApi")); assert!(hint.contains("Phase 14")); } #[test] fn harness_has_base64_decoder() { let spec = make_spec(PayloadSlot::Param(0)); let harness = emit(&spec).unwrap(); assert!(harness.source.contains("Base64.getDecoder()")); assert!(harness.source.contains("NYX_PAYLOAD_B64")); } // ── Phase 14: shape detection ──────────────────────────────────────────── fn make_spec_with(kind: EntryKind, name: &str, entry_file: &str) -> HarnessSpec { let mut s = make_spec(PayloadSlot::Param(0)); s.entry_kind = kind; s.entry_name = name.to_owned(); s.entry_file = entry_file.to_owned(); s } #[test] fn shape_detect_servlet_doget() { let src = "import javax.servlet.http.HttpServletRequest;\npublic class V extends HttpServlet { public void doGet(HttpServletRequest r, HttpServletResponse w) {} }"; let spec = make_spec_with(EntryKind::HttpRoute, "doGet", "V.java"); assert_eq!(JavaShape::detect(&spec, src), JavaShape::ServletDoGet); } #[test] fn shape_detect_servlet_dopost() { let src = "import jakarta.servlet.http.HttpServletRequest;\npublic class V extends HttpServlet { public void doPost(HttpServletRequest r, HttpServletResponse w) {} }"; let spec = make_spec_with(EntryKind::HttpRoute, "doPost", "V.java"); assert_eq!(JavaShape::detect(&spec, src), JavaShape::ServletDoPost); } #[test] fn shape_detect_spring_controller() { let src = "@RestController\npublic class V { @GetMapping(\"/x\") public String run(String p) { return p; } }"; let spec = make_spec_with(EntryKind::HttpRoute, "run", "V.java"); assert_eq!(JavaShape::detect(&spec, src), JavaShape::SpringController); } #[test] fn shape_detect_quarkus_route() { let src = "import jakarta.ws.rs.GET;\n@Path(\"/x\")\npublic class V { @GET public String run(String p) { return p; } }"; let spec = make_spec_with(EntryKind::HttpRoute, "run", "V.java"); assert_eq!(JavaShape::detect(&spec, src), JavaShape::QuarkusRoute); } #[test] fn shape_detect_micronaut_route() { let src = "import io.micronaut.http.annotation.Controller;\nimport io.micronaut.http.annotation.Get;\n@Controller(\"/x\")\npublic class V { @Get(\"/y\") public String run(String p) { return p; } }"; let spec = make_spec_with(EntryKind::HttpRoute, "run", "V.java"); assert_eq!(JavaShape::detect(&spec, src), JavaShape::MicronautRoute); } #[test] fn shape_detect_static_main() { let src = "public class V { public static void main(String[] args) {} }"; let spec = make_spec_with(EntryKind::CliSubcommand, "main", "V.java"); assert_eq!(JavaShape::detect(&spec, src), JavaShape::StaticMain); } #[test] fn shape_detect_junit_test() { let src = "import org.junit.jupiter.api.Test;\npublic class V { @Test public void testRun() {} }"; let spec = make_spec_with(EntryKind::Function, "testRun", "V.java"); assert_eq!(JavaShape::detect(&spec, src), JavaShape::JunitTest); } #[test] fn shape_detect_static_method_fallback() { let src = "public class V { public static void run(String p) {} }"; let spec = make_spec_with(EntryKind::Function, "run", "V.java"); assert_eq!(JavaShape::detect(&spec, src), JavaShape::StaticMethod); } #[test] fn servlet_shape_emits_reflective_invocation() { let spec = make_spec_with(EntryKind::HttpRoute, "doGet", "Vuln.java"); let src = generate_harness_java(&spec, JavaShape::ServletDoGet, "Vuln"); assert!(src.contains("invokeServlet(Vuln.class")); assert!(src.contains("buildRequestStub")); } #[test] fn spring_shape_emits_reflective_invocation() { let spec = make_spec_with(EntryKind::HttpRoute, "run", "Vuln.java"); let src = generate_harness_java(&spec, JavaShape::SpringController, "Vuln"); assert!(src.contains("invokeReflective(Vuln.class, \"run\"")); } #[test] fn quarkus_shape_emits_reflective_invocation() { let spec = make_spec_with(EntryKind::HttpRoute, "run", "Vuln.java"); let src = generate_harness_java(&spec, JavaShape::QuarkusRoute, "Vuln"); assert!(src.contains("invokeReflective(Vuln.class, \"run\"")); } #[test] fn micronaut_shape_emits_reflective_invocation() { let spec = make_spec_with(EntryKind::HttpRoute, "run", "Vuln.java"); let src = generate_harness_java(&spec, JavaShape::MicronautRoute, "Vuln"); assert!(src.contains("invokeReflective(Vuln.class, \"run\"")); } #[test] fn spring_shape_emits_marker_when_with_spring_test() { let mut spec = make_spec_with(EntryKind::HttpRoute, "run", "Vuln.java"); spec.java_toolchain.with_spring_test = true; let src = generate_harness_java(&spec, JavaShape::SpringController, "Vuln"); assert!(src.contains("NYX_SPRING_TEST=1")); let mut off = make_spec_with(EntryKind::HttpRoute, "run", "Vuln.java"); off.java_toolchain.with_spring_test = false; let src_off = generate_harness_java(&off, JavaShape::SpringController, "Vuln"); assert!(!src_off.contains("NYX_SPRING_TEST=1")); } #[test] fn static_main_shape_passes_argv() { let spec = make_spec_with(EntryKind::CliSubcommand, "main", "Vuln.java"); let src = generate_harness_java(&spec, JavaShape::StaticMain, "Vuln"); assert!(src.contains("Vuln.main(mainArgs)")); assert!(src.contains("new String[] { payload }")); } #[test] fn junit_shape_emits_reflective_invocation() { let spec = make_spec_with(EntryKind::Function, "testRun", "Vuln.java"); let src = generate_harness_java(&spec, JavaShape::JunitTest, "Vuln"); assert!(src.contains("invokeJunitTest(Vuln.class")); } // ── Servlet stub bundle (path (a) of Phase 31 budget gate) ────────────── fn stage_entry(dir: &std::path::Path, name: &str, body: &str) -> String { let path = dir.join(name); std::fs::write(&path, body).expect("stage java entry source"); path.to_string_lossy().into_owned() } #[test] fn emit_servlet_doget_carries_servlet_stub_bundle() { let tmp = tempfile::TempDir::new().unwrap(); let entry_file = stage_entry( tmp.path(), "Vuln.java", "import javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\npublic class Vuln {\n public void doGet(HttpServletRequest r, HttpServletResponse w) {}\n}\n", ); let mut spec = make_spec_with(EntryKind::HttpRoute, "doGet", &entry_file); spec.payload_slot = PayloadSlot::QueryParam("payload".into()); let harness = emit(&spec).unwrap(); let paths: Vec<&str> = harness .extra_files .iter() .map(|(p, _)| p.as_str()) .collect(); assert!( paths.contains(&"javax/servlet/http/HttpServletRequest.java"), "doGet bundle missing javax HttpServletRequest stub; got {paths:?}" ); assert!( paths.contains(&"jakarta/servlet/http/HttpServletRequest.java"), "doGet bundle missing jakarta HttpServletRequest stub; got {paths:?}" ); assert!(paths.contains(&"javax/servlet/annotation/WebServlet.java")); assert!(paths.contains(&"javax/servlet/ServletException.java")); } #[test] fn emit_servlet_dopost_carries_servlet_stub_bundle() { let tmp = tempfile::TempDir::new().unwrap(); let entry_file = stage_entry( tmp.path(), "Vuln.java", "import jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\npublic class Vuln {\n public void doPost(HttpServletRequest r, HttpServletResponse w) {}\n}\n", ); let mut spec = make_spec_with(EntryKind::HttpRoute, "doPost", &entry_file); spec.payload_slot = PayloadSlot::HttpBody; let harness = emit(&spec).unwrap(); assert!(!harness.extra_files.is_empty(), "doPost bundle is empty"); let paths: Vec<&str> = harness .extra_files .iter() .map(|(p, _)| p.as_str()) .collect(); assert!(paths.contains(&"javax/servlet/http/HttpServlet.java")); assert!(paths.contains(&"jakarta/servlet/http/HttpServlet.java")); } #[test] fn emit_static_method_carries_no_extra_files() { // Regression guard: non-servlet shapes must not pay the servlet // stub cost. Adding stubs would balloon the workdir + compile // time for every Rust / Python / etc. harness too. let spec = make_spec(PayloadSlot::Param(0)); let harness = emit(&spec).unwrap(); assert!( harness.extra_files.is_empty(), "non-servlet shape unexpectedly ships extra files: {:?}", harness .extra_files .iter() .map(|(p, _)| p) .collect::>() ); } #[test] fn emit_static_main_carries_no_extra_files() { let tmp = tempfile::TempDir::new().unwrap(); let entry_file = stage_entry( tmp.path(), "Vuln.java", "public class Vuln { public static void main(String[] args) {} }\n", ); let spec = make_spec_with(EntryKind::CliSubcommand, "main", &entry_file); let harness = emit(&spec).unwrap(); assert!(harness.extra_files.is_empty()); } #[test] fn emit_servlet_doget_bundles_owasp_stubs_when_source_imports_owasp() { let tmp = tempfile::TempDir::new().unwrap(); let entry_file = stage_entry( tmp.path(), "BenchmarkTest00001.java", "package org.owasp.benchmark.testcode;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport javax.servlet.http.HttpServlet;\nimport org.owasp.benchmark.helpers.Utils;\nimport org.owasp.esapi.ESAPI;\npublic class BenchmarkTest00001 extends HttpServlet {\n public void doGet(HttpServletRequest r, HttpServletResponse w) {}\n}\n", ); let spec = make_spec_with(EntryKind::HttpRoute, "doGet", &entry_file); let harness = emit(&spec).unwrap(); let paths: Vec<&str> = harness .extra_files .iter() .map(|(p, _)| p.as_str()) .collect(); // Servlet stubs are present (same as the non-OWASP servlet case). assert!(paths.contains(&"javax/servlet/http/HttpServletRequest.java")); // OWASP helpers + esapi + spring stubs are appended. assert!(paths.contains(&"org/owasp/benchmark/helpers/Utils.java")); assert!(paths.contains(&"org/owasp/esapi/ESAPI.java")); assert!(paths.contains(&"org/owasp/benchmark/helpers/DatabaseHelper.java")); assert!(paths.contains(&"org/springframework/jdbc/core/RowMapper.java")); } #[test] fn emit_servlet_doget_skips_owasp_stubs_when_source_is_plain() { // Servlet entry without OWASP / Spring imports must only carry // the servlet stub bundle, not the OWASP add-on. Keeps workdir // small for the existing servlet_doget fixture path. let tmp = tempfile::TempDir::new().unwrap(); let entry_file = stage_entry( tmp.path(), "Vuln.java", "import javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\npublic class Vuln {\n public void doGet(HttpServletRequest r, HttpServletResponse w) {}\n}\n", ); let spec = make_spec_with(EntryKind::HttpRoute, "doGet", &entry_file); let harness = emit(&spec).unwrap(); let paths: Vec<&str> = harness .extra_files .iter() .map(|(p, _)| p.as_str()) .collect(); assert!( !paths.iter().any(|p| p.starts_with("org/owasp/")), "plain servlet entry unexpectedly bundles OWASP stubs: {paths:?}" ); assert!( !paths.iter().any(|p| p.starts_with("org/springframework/")), "plain servlet entry unexpectedly bundles Spring stubs: {paths:?}" ); } #[test] fn emit_static_method_with_owasp_imports_bundles_helpers() { // Non-servlet shapes still need the OWASP stub set when the // entry source pulls in helpers (e.g. a plain @Test fixture // calling `Utils.encodeForHTML`). let tmp = tempfile::TempDir::new().unwrap(); let entry_file = stage_entry( tmp.path(), "Vuln.java", "import org.owasp.benchmark.helpers.Utils;\npublic class Vuln {\n public static void run(String p) { Utils.encodeForHTML(p); }\n}\n", ); let spec = make_spec_with(EntryKind::Function, "run", &entry_file); let harness = emit(&spec).unwrap(); let paths: Vec<&str> = harness .extra_files .iter() .map(|(p, _)| p.as_str()) .collect(); assert!(paths.contains(&"org/owasp/benchmark/helpers/Utils.java")); // No servlet stubs for a non-servlet shape. assert!(!paths.iter().any(|p| p.starts_with("javax/servlet/"))); } #[test] fn parse_package_name_handles_packaged_source() { assert_eq!( parse_package_name("package org.owasp.benchmark.testcode;\nclass X {}\n"), Some("org.owasp.benchmark.testcode".to_owned()) ); // Leading whitespace + extra spaces inside the line are tolerated. assert_eq!( parse_package_name(" package a.b.c ;\n"), Some("a.b.c".to_owned()) ); // Leading comments / blank lines must not cause an early miss. assert_eq!( parse_package_name("// header comment\n/* block */\npackage com.example;\n"), Some("com.example".to_owned()) ); } #[test] fn parse_package_name_returns_none_when_absent() { assert_eq!(parse_package_name(""), None); assert_eq!(parse_package_name("public class X {}\n"), None); } #[test] fn derive_entry_qualifier_uses_package_when_present() { let src = "package org.owasp.benchmark.testcode;\npublic class BenchmarkTest00001 {}\n"; assert_eq!( derive_entry_qualifier(src, "BenchmarkTest00001"), "org.owasp.benchmark.testcode.BenchmarkTest00001" ); } #[test] fn derive_entry_qualifier_falls_back_to_simple_name() { assert_eq!(derive_entry_qualifier("", "Vuln"), "Vuln"); assert_eq!( derive_entry_qualifier("public class Vuln {}", "Vuln"), "Vuln" ); } #[test] fn emit_static_method_with_packaged_source_uses_fqn_in_harness() { // Packaged entry sources must be addressed by FQN in the // generated NyxHarness, otherwise javac fails with // `cannot find symbol: class ` because the // packaged .class lives under `org/owasp/.../.class` // and NyxHarness itself sits in the default package. let tmp = tempfile::TempDir::new().unwrap(); let entry_file = stage_entry( tmp.path(), "Vuln.java", "package org.example;\npublic class Vuln { public static void run(String p) {} }\n", ); let spec = make_spec_with(EntryKind::Function, "run", &entry_file); let harness = emit(&spec).unwrap(); assert!( harness.source.contains("org.example.Vuln.run(payload)"), "harness must address packaged entry via FQN; got source:\n{}", harness.source ); } #[test] fn emit_spring_controller_carries_no_servlet_stubs() { // Spring controllers do not import `javax.servlet.*`; shipping // the bundle would still compile fine but adds dead `.class` // files to the workdir. Keep the bundle scoped to actual // servlet shapes. let tmp = tempfile::TempDir::new().unwrap(); let entry_file = stage_entry( tmp.path(), "Vuln.java", "@RestController\npublic class Vuln {\n @GetMapping(\"/x\") public String run(String p) { return p; }\n}\n", ); let mut spec = make_spec_with(EntryKind::HttpRoute, "run", &entry_file); spec.payload_slot = PayloadSlot::Param(0); let harness = emit(&spec).unwrap(); assert!(harness.extra_files.is_empty()); } #[test] fn entry_class_parses_public_class_declaration() { assert_eq!(derive_entry_class("public class Vuln {}"), "Vuln"); assert_eq!(derive_entry_class("public final class Foo {}"), "Foo"); assert_eq!(derive_entry_class("public abstract class Bar {}"), "Bar"); // No public class → "Entry" fallback. assert_eq!(derive_entry_class(""), "Entry"); assert_eq!(derive_entry_class("class Pkg {}"), "Entry"); } #[test] fn entry_subpath_matches_public_class() { let mut spec = make_spec(PayloadSlot::Param(0)); // Path does not exist on disk → derive_entry_class falls back // to "Entry" → subpath is "Entry.java". spec.entry_file = "/nonexistent/Vuln.java".into(); let harness = emit(&spec).unwrap(); assert_eq!(harness.entry_subpath, Some("Entry.java".to_owned())); } #[test] fn probe_shim_publishes_stub_http_recorder() { let shim = probe_shim(); assert!( shim.contains("static void __nyx_stub_http_record"), "Java probe shim must define __nyx_stub_http_record" ); assert!( shim.contains("\"NYX_HTTP_LOG\""), "Java HTTP recorder must read NYX_HTTP_LOG to find the side-channel log" ); assert!( shim.contains("\"method: \""), "Java HTTP recorder must emit a method detail line" ); assert!( shim.contains("\"url: \""), "Java HTTP recorder must emit a url detail line" ); } #[test] fn probe_shim_publishes_stub_sql_recorder() { let shim = probe_shim(); assert!( shim.contains("static void __nyx_stub_sql_record"), "Java probe shim must define __nyx_stub_sql_record" ); assert!( shim.contains("\"NYX_SQL_LOG\""), "Java SQL recorder must read NYX_SQL_LOG to find the side-channel log" ); assert!( shim.contains("query.endsWith(\"\\n\")"), "Java SQL recorder must guarantee a trailing newline on the query line so SqlStub::drain_events frames each record" ); } #[test] fn chain_step_splices_probe_shim_for_composite_reverify() { let step = chain_step(Some(b""), None); assert!( step.source.contains("__nyx_probe"), "Java chain step must splice the probe shim" ); assert!( step.source.starts_with("public class Step {"), "Java chain step must open with the `public class Step {{` declaration" ); assert!( step.source.contains("System.getenv(\"NYX_PREV_OUTPUT\")"), "Java chain step must keep its NYX_PREV_OUTPUT forwarder" ); let shim_pos = step.source.find("__nyx_probe").unwrap(); let driver_pos = step .source .find("System.getenv(\"NYX_PREV_OUTPUT\")") .unwrap(); assert!( shim_pos < driver_pos, "probe shim must come before the driver so the shim's helpers are in scope when a sink rewrite splices in" ); let main_pos = step.source.find("public static void main").unwrap(); assert!( shim_pos < main_pos, "probe shim members must be declared before `main` so the class compiles cleanly" ); assert_eq!(step.filename, "Step.java"); } #[test] fn detect_shape_reads_file_and_returns_shape() { // Drive the public `detect_shape(spec)` wrapper end-to-end: // write a representative source to a tempfile, then assert the // wrapper reads it and produces the expected JavaShape variant. let dir = std::env::temp_dir().join(format!("nyx_detect_shape_{}", std::process::id())); let _ = std::fs::create_dir_all(&dir); let cases: &[(&str, &str, &str, EntryKind, JavaShape)] = &[ ( "Servlet.java", "import javax.servlet.http.HttpServletRequest;\npublic class Servlet extends HttpServlet { public void doGet(HttpServletRequest r, HttpServletResponse w) {} }", "doGet", EntryKind::HttpRoute, JavaShape::ServletDoGet, ), ( "Spring.java", "@RestController\npublic class Spring { @GetMapping(\"/x\") public String run(String p) { return p; } }", "run", EntryKind::HttpRoute, JavaShape::SpringController, ), ( "MainClass.java", "public class MainClass { public static void main(String[] args) {} }", "main", EntryKind::CliSubcommand, JavaShape::StaticMain, ), ( "Plain.java", "public class Plain { public static void run(String p) {} }", "run", EntryKind::Function, JavaShape::StaticMethod, ), ]; for (name, body, entry_name, kind, expected) in cases { let path = dir.join(name); std::fs::write(&path, body).expect("write fixture"); let spec = make_spec_with(kind.clone(), entry_name, path.to_str().unwrap()); assert_eq!(detect_shape(&spec), *expected, "case {name}"); } let _ = std::fs::remove_dir_all(&dir); } fn make_ldap_spec() -> HarnessSpec { let mut s = make_spec(PayloadSlot::Param(0)); s.expected_cap = Cap::LDAP_INJECTION; s.entry_name = "run".into(); s } #[test] fn emit_ldap_harness_routes_through_stub_when_endpoint_set() { let h = emit_ldap_harness(&make_ldap_spec()); assert!( h.source.contains("NYX_LDAP_ENDPOINT"), "Java LDAP harness must read NYX_LDAP_ENDPOINT to route through the stub", ); assert!( h.source.contains("javax.naming.directory.InitialDirContext"), "Java LDAP harness must import the JNDI InitialDirContext for the BER round-trip", ); assert!( h.source.contains("new InitialDirContext(env)"), "Java LDAP harness must construct an InitialDirContext bound at the stub endpoint", ); assert!( h.source.contains("\"ldap://\" + ep + \"/\""), "Java LDAP harness must compose an ldap:// PROVIDER_URL from NYX_LDAP_ENDPOINT", ); assert!( h.source.contains("ctx.search(\"\", filter, controls)"), "Java LDAP harness must dispatch DirContext.search over LDAPv3 BER", ); assert!( h.source.contains("com.sun.jndi.ldap.LdapCtxFactory"), "Java LDAP harness must select the JDK LDAP context factory", ); } #[test] fn emit_ldap_harness_retains_local_matcher_fallback() { let h = emit_ldap_harness(&make_ldap_spec()); assert!( h.source.contains("nyxLdapCountLocal"), "Java LDAP harness must keep the in-process matcher as a fallback for hosts without the stub", ); assert!( h.source.contains("nyxLdapCountViaJndi"), "Java LDAP harness must dispatch through the JNDI stub-route helper", ); } fn write_servlet_fixture(dir: &std::path::Path, body: &str) -> String { let path = dir.join("Vuln.java"); std::fs::write(&path, body).unwrap(); path.to_string_lossy().into_owned() } #[test] fn emit_header_injection_harness_drives_fixture_through_stub_when_servlet_present() { let dir = std::env::temp_dir().join("nyx_phase08_test_drive_fixture"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let entry = write_servlet_fixture( &dir, "import javax.servlet.http.HttpServletResponse;\n\ public class Vuln {\n public static void run(HttpServletResponse r, String v) {\n r.setHeader(\"Set-Cookie\", v);\n }\n}\n", ); let mut spec = make_spec(PayloadSlot::Param(0)); spec.expected_cap = Cap::HEADER_INJECTION; spec.entry_file = entry; spec.entry_name = "run".into(); let h = emit_header_injection_harness(&spec); assert!( !h.extra_files.is_empty(), "servlet-importing fixture must trigger stub-file emission", ); assert!( h.source.contains("HttpServletResponse response = new javax.servlet.http.HttpServletResponse()"), "Java HEADER_INJECTION harness must instantiate the captured-header response wrapper", ); assert!( h.source.contains("Class.forName(\"Vuln\")"), "Java HEADER_INJECTION harness must reflectively load the fixture entry class", ); assert!( h.source.contains("nyxDrainHeaders()"), "Java HEADER_INJECTION harness must drain captured headers after invoking the fixture", ); assert!( h.source.contains("for (String[] pair : captured)"), "Java HEADER_INJECTION harness must emit one probe per captured (name, value) pair", ); let _ = std::fs::remove_dir_all(&dir); } #[test] fn emit_header_injection_harness_uses_jakarta_namespace_for_jakarta_imports() { let dir = std::env::temp_dir().join("nyx_phase08_test_jakarta_ns"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let entry = write_servlet_fixture( &dir, "import jakarta.servlet.http.HttpServletResponse;\n\ public class Vuln {\n public static void run(HttpServletResponse r, String v) {\n r.setHeader(\"Set-Cookie\", v);\n }\n}\n", ); let mut spec = make_spec(PayloadSlot::Param(0)); spec.expected_cap = Cap::HEADER_INJECTION; spec.entry_file = entry; spec.entry_name = "run".into(); let h = emit_header_injection_harness(&spec); assert!( h.source.contains("jakarta.servlet.http.HttpServletResponse"), "Java HEADER_INJECTION harness must follow the entry source's servlet namespace", ); assert!( !h.source.contains("javax.servlet.http.HttpServletResponse response"), "Jakarta entry must not instantiate javax response wrapper", ); let _ = std::fs::remove_dir_all(&dir); } #[test] fn emit_header_injection_harness_falls_back_to_synthetic_probe_without_servlet() { let dir = std::env::temp_dir().join("nyx_phase08_test_no_servlet"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let entry = write_servlet_fixture( &dir, "public class Vuln { public static void run(String v) { System.out.println(v); } }\n", ); let mut spec = make_spec(PayloadSlot::Param(0)); spec.expected_cap = Cap::HEADER_INJECTION; spec.entry_file = entry; spec.entry_name = "run".into(); let h = emit_header_injection_harness(&spec); assert!( h.extra_files.is_empty(), "non-servlet fixture must not ship servlet stubs", ); assert!( !h.source.contains("nyxDrainHeaders()"), "non-servlet fixture must skip the stub-driven capture path", ); assert!( h.source.contains("nyxHeaderProbe(\"Set-Cookie\", payload)"), "non-servlet fixture must keep the synthetic-probe fallback", ); let _ = std::fs::remove_dir_all(&dir); } #[test] fn emit_open_redirect_harness_drives_fixture_through_stub_when_servlet_present() { let dir = std::env::temp_dir().join("nyx_phase09_test_drive_fixture"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let entry = write_servlet_fixture( &dir, "import javax.servlet.http.HttpServletResponse;\n\ public class Vuln {\n public static void run(HttpServletResponse r, String v) throws Exception {\n r.sendRedirect(v);\n }\n}\n", ); let mut spec = make_spec(PayloadSlot::Param(0)); spec.expected_cap = Cap::OPEN_REDIRECT; spec.entry_file = entry; spec.entry_name = "run".into(); let h = emit_open_redirect_harness(&spec); assert!( !h.extra_files.is_empty(), "servlet-importing fixture must trigger stub-file emission", ); assert!( h.source.contains("HttpServletResponse response = new javax.servlet.http.HttpServletResponse()"), "Java OPEN_REDIRECT harness must instantiate the captured-redirect response wrapper", ); assert!( h.source.contains("Class.forName(\"Vuln\")"), "Java OPEN_REDIRECT harness must reflectively load the fixture entry class", ); assert!( h.source.contains("response.getRedirectedUrl()"), "Java OPEN_REDIRECT harness must read the captured Location: value from the stub", ); let _ = std::fs::remove_dir_all(&dir); } #[test] fn emit_open_redirect_harness_falls_back_to_synthetic_probe_without_servlet() { let dir = std::env::temp_dir().join("nyx_phase09_test_no_servlet"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let entry = write_servlet_fixture( &dir, "public class Vuln { public static void run(String v) { System.out.println(v); } }\n", ); let mut spec = make_spec(PayloadSlot::Param(0)); spec.expected_cap = Cap::OPEN_REDIRECT; spec.entry_file = entry; spec.entry_name = "run".into(); let h = emit_open_redirect_harness(&spec); assert!( h.extra_files.is_empty(), "non-servlet fixture must not ship servlet stubs", ); assert!( !h.source.contains("response.getRedirectedUrl()"), "non-servlet fixture must skip the stub-driven capture path", ); assert!( h.source.contains("nyxRedirectProbe(payload, requestHost)"), "non-servlet fixture must keep the synthetic-probe fallback", ); let _ = std::fs::remove_dir_all(&dir); } #[test] fn emit_open_redirect_harness_ships_follow_location_helper() { let dir = std::env::temp_dir().join("nyx_phase09_test_follow_helper"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let entry = write_servlet_fixture( &dir, "public class Vuln { public static void run(String v) { System.out.println(v); } }\n", ); let mut spec = make_spec(PayloadSlot::Param(0)); spec.expected_cap = Cap::OPEN_REDIRECT; spec.entry_file = entry; spec.entry_name = "run".into(); let h = emit_open_redirect_harness(&spec); assert!( h.source.contains("static void nyxFollowLocation(String location)"), "OPEN_REDIRECT harness must declare the nyxFollowLocation helper", ); assert!( h.source.contains("import java.net.HttpURLConnection;"), "OPEN_REDIRECT harness must import HttpURLConnection", ); assert!( h.source.contains("import java.net.URL;"), "OPEN_REDIRECT harness must import URL", ); assert!( h.source.contains("http://127.0.0.1"), "follow-location helper must whitelist loopback hosts", ); assert!( h.source.contains("nyxFollowLocation(payload)"), "tier-(b) fallback must follow the synthetic payload location", ); let _ = std::fs::remove_dir_all(&dir); } #[test] fn emit_open_redirect_harness_follows_captured_location_in_tier_a() { let dir = std::env::temp_dir().join("nyx_phase09_test_follow_tier_a"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let entry = write_servlet_fixture( &dir, "import javax.servlet.http.HttpServletResponse;\n\ public class Vuln {\n public static void run(HttpServletResponse r, String v) throws Exception {\n r.sendRedirect(v);\n }\n}\n", ); let mut spec = make_spec(PayloadSlot::Param(0)); spec.expected_cap = Cap::OPEN_REDIRECT; spec.entry_file = entry; spec.entry_name = "run".into(); let h = emit_open_redirect_harness(&spec); assert!( h.source.contains("nyxRedirectProbe(captured, requestHost);\n nyxFollowLocation(captured);"), "tier-(a) must follow the captured Location: value, not the raw payload", ); let _ = std::fs::remove_dir_all(&dir); } #[test] fn emit_xpath_harness_routes_through_real_xpath_reflectively() { let dir = std::env::temp_dir().join("nyx_phase07_test_drive_fixture"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let entry = write_servlet_fixture( &dir, "import javax.xml.xpath.XPath;\n\ import javax.xml.xpath.XPathConstants;\n\ public class Vuln {\n public static Object run(String name) throws Exception { return null; }\n}\n", ); let mut spec = make_spec(PayloadSlot::Param(0)); spec.expected_cap = Cap::XPATH_INJECTION; spec.entry_file = entry; spec.entry_name = "run".into(); let h = emit_xpath_harness(&spec); assert!( !h.extra_files.is_empty(), "XPath harness must stage the canonical corpus XML", ); assert!( h.source.contains("import org.w3c.dom.NodeList;"), "tier-(a) harness must import NodeList for the cast", ); assert!( h.source.contains("Class.forName(\"Vuln\")"), "tier-(a) harness must reflectively load the fixture entry class", ); assert!( h.source.contains("getDeclaredMethod(\"run\", String.class)"), "tier-(a) harness must reflectively grab the fixture's run(String) method", ); assert!( h.source.contains("((NodeList) result).getLength()"), "tier-(a) harness must cast the result to NodeList and count nodes", ); assert!( h.source.contains("__NYX_XPATH_TIER_A__"), "tier-(a) harness must emit the tier-(a) stdout marker after the real reflective invoke: {}", h.source ); let _ = std::fs::remove_dir_all(&dir); } #[test] fn emit_xpath_harness_drops_inline_matcher_fallback() { let dir = std::env::temp_dir().join("nyx_phase07_test_no_inline_matcher"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let entry = write_servlet_fixture( &dir, "public class Vuln { public static Object run(String name) { return null; } }\n", ); let mut spec = make_spec(PayloadSlot::Param(0)); spec.expected_cap = Cap::XPATH_INJECTION; spec.entry_file = entry; spec.entry_name = "run".into(); let h = emit_xpath_harness(&spec); assert!( !h.source.contains("nyxXpathSelect"), "harness must not carry the inline `nyxXpathSelect` matcher; tier-(a) reflective invoke is the only path", ); assert!( !h.source.contains("NYX_XPATH_USERS"), "harness must not carry the inline `NYX_XPATH_USERS` table; tier-(a) reflective invoke is the only path", ); assert!( h.source.contains("NYX_IMPORT_ERROR:") && h.source.contains("System.exit(77)"), "harness must emit `NYX_IMPORT_ERROR:` stderr marker + `System.exit(77)` on reflective lookup failure: {}", h.source ); assert!( h.source.contains("__NYX_XPATH_TIER_A__"), "harness must emit the tier-(a) stdout marker: {}", h.source ); assert!( h.source.contains("import org.w3c.dom.NodeList;") && h.source.contains("import java.lang.reflect.Method;"), "harness must always import the reflective invocation path; the synthetic-only branch is gone", ); } }