//! 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 drives the /// controller through Spring MockMvc so annotation mapping and /// request binding stay in the execution path. 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 replays a JAX-RS request shape through the real /// Jakarta annotations instead of calling the entry by name only. QuarkusRoute, /// Micronaut route: `@Controller("/api")` + `@Get`/`@Post`/`@Put` /// /`@Delete` on a method. Harness replays the controller route /// through Micronaut's runtime annotations and path binding shape. 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(); let mut maven_deps: Vec = Vec::new(); let mut seen_maven: std::collections::HashSet<(&'static str, &'static str)> = std::collections::HashSet::new(); if let Some(adapter) = env.framework_adapter.as_deref() { for dep in crate::dynamic::framework::runtime_deps::deps_for_adapter(adapter).maven_packages { if seen_maven.insert((dep.group_id, dep.artifact_id)) { maven_deps.push(*dep); } } } 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() || !maven_deps.is_empty() { body.push_str(" \n"); for dep in &maven_deps { body.push_str(" \n"); body.push_str(&format!(" {}\n", dep.group_id)); body.push_str(&format!( " {}\n", dep.artifact_id )); body.push_str(&format!(" {}\n", dep.version)); 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 11 (Track J.9): CRYPTO weak-RNG short-circuit. The Java // harness reflectively loads the fixture class, invokes its // declared method with the payload, and reduces the produced key // into a `ProbeKind::WeakKey { key_int }` record (byte[] → // `ByteBuffer.wrap(zero-padded[8]).order(BIG_ENDIAN).getLong()`; // `Number` subclasses → `longValue()`). A weak // `java.util.Random.nextBytes(new byte[2])` reduces to a sub-2^16 // key_int; a `SecureRandom.nextBytes(new byte[32])` head-8 byte // view overshoots the 16-bit budget. if spec.expected_cap == crate::labels::Cap::CRYPTO { return Ok(emit_crypto_harness(spec)); } // Phase 11 (Track J.9): JSON_PARSE depth-bomb short-circuit. The // Java harness reflectively loads the fixture class, invokes its // declared method with the payload, walks the returned tree // iteratively via `NyxJsonProbe.countDepth`, and emits a // [`crate::dynamic::probe::ProbeKind::JsonParse`] probe. The // hand-rolled `NyxJsonProbe` helper is shipped as a sibling // `.java` file so the build path never reaches for Jackson / // Gson. if spec.expected_cap == crate::labels::Cap::JSON_PARSE { return Ok(emit_json_parse_harness(spec)); } // Phase 11 (Track J.9): UNAUTHORIZED_ID IDOR boundary harness. // Reflectively loads the fixture entry class, invokes the named // static method with the payload as `owner_id`, and emits a // `ProbeKind::IdorAccess { caller_id, owner_id }` probe only when // the fixture returns a non-`null` record. The benign fixture's // `if (!CALLER.equals(ownerId)) return null;` rejection clears the // probe; the vuln fixture's unguarded `STORE.get(ownerId)` always // materialises a record so the probe fires for every cross-tenant // payload. if spec.expected_cap == crate::labels::Cap::UNAUTHORIZED_ID { return Ok(emit_unauthorized_id_harness(spec)); } // Phase 11 (Track J.9): DATA_EXFIL outbound-network harness. Java // has no stdlib monkey-patch hook, so the harness ships a sibling // `NyxMockHttp.java` helper the fixture calls into in place of // `HttpURLConnection.openConnection().connect()`. `NyxMockHttp.get` // captures the destination host into a shared list without // initiating real wire I/O; the harness then drains the list and // emits a `ProbeKind::OutboundNetwork { host }` probe per call. if spec.expected_cap == crate::labels::Cap::DATA_EXFIL { return Ok(emit_data_exfil_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()); } // FILE_IO (path-traversal) entry-driven confirmation: plant a canary at the // workdir root whose CONTENT is the collision-resistant marker, plus an // empty `testfiles/` directory so the `../nyx_pt_canary` payload resolves // (`/testfiles/../nyx_pt_canary` → `/nyx_pt_canary`). The // Utils stub points `testfileDir` at `/testfiles/` (via // `System.getProperty("user.dir")`), and the harness CWD is the workdir. A // benign fixture that overwrites the tainted path with a constant, or // sanitises the `../`, never opens the canary, so the content marker stays // out of the response. if spec.expected_cap == crate::labels::Cap::FILE_IO { extra_files.push(("testfiles/.nyxkeep".to_owned(), String::new())); extra_files.push(( crate::dynamic::corpus::path_trav::java::CANARY_FILENAME.to_owned(), crate::dynamic::corpus::path_trav::java::CANARY_MARKER.to_owned(), )); } Ok(HarnessSource { source, filename: "NyxHarness.java".to_owned(), command: vec![ "java".to_owned(), "-cp".to_owned(), ".:lib/*".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(); // Tier-(a) main: drive the fixture's enclosing entry with the forged // blob so a caller-side mitigation (a `resolveClass` allowlist / // restricted ObjectInputStream subclass) runs before the gadget class // is resolved. Detection is by exception type: a vanilla // ObjectInputStream reaches `resolveClass(gadget)` and raises // ClassNotFoundException (the gadget is not on the classpath) — that is // unrestricted deserialization, so a probe fires. A guarded fixture // raises InvalidClassException at its allowlist check *before* the // class resolves, so no probe is written. Falls back to the tier-(b) // synthetic restricted-OIS path when reflection setup fails. let main_body = if spec.entry_is_derivable() { let class_name = java_entry_class_name(spec); let method_name = &spec.entry_name; format!( r#" public static void main(String[] args) {{ String payload = System.getenv("NYX_PAYLOAD"); if (payload == null) payload = ""; String prefix = "NYX_GADGET_CLASS:"; boolean drove = false; if (payload.startsWith(prefix)) {{ String cls = payload.substring(prefix.length()); // Tier-(a): drive `{class_name}.{method_name}(byte[])` so the // fixture's own (un)restricted deserialization path runs. try {{ byte[] blob = nyxForgeClassDescriptor(cls); Class entryCls = Class.forName("{class_name}"); java.lang.reflect.Method m = entryCls.getMethod("{method_name}", byte[].class); drove = true; try {{ m.invoke(null, (Object) blob); }} catch (java.lang.reflect.InvocationTargetException ite) {{ if (nyxCauseChainHas(ite.getCause(), ClassNotFoundException.class)) {{ // The fixture's deserializer reached and tried to // resolve the gadget class (unrestricted path). nyxDeserializeProbe(true); }} // InvalidClassException (a caller-side allowlist block) // lands here too but is not a ClassNotFoundException, so // a guarded fixture writes no probe. }} catch (Throwable t) {{ // Other reflective-call failure — non-probe path. }} }} catch (Throwable setup) {{ // Reflection setup failed (class / method missing) — fall // through to the tier-(b) synthetic path below. drove = false; }} }} if (!drove) {{ // Tier-(b): the enclosing entry could not be driven — synthetic // restricted-OIS direct path (recorded as direct-sink fallback). nyxSyntheticDeserialize(payload); }} // 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__"); }} /// True when `t` or any exception in its cause chain is an instance of /// `want` — used to detect the gadget-class resolution attempt that a /// vanilla ObjectInputStream surfaces as ClassNotFoundException. static boolean nyxCauseChainHas(Throwable t, Class want) {{ int hops = 0; while (t != null && hops < 32) {{ if (want.isInstance(t)) return true; t = t.getCause(); hops++; }} return false; }} "# ) } else { // No derivable enclosing entry — drive the synthetic restricted-OIS // path directly. r#" public static void main(String[] args) { String payload = System.getenv("NYX_PAYLOAD"); if (payload == null) payload = ""; nyxSyntheticDeserialize(payload); // 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__"); } "# .to_owned() }; 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(); }} /// Tier-(b) synthetic direct-sink: run the forged blob through a /// restricted ObjectInputStream the harness controls. Bypasses any /// caller-side guard, so it is used only when the fixture's own entry /// could not be driven. static void nyxSyntheticDeserialize(String payload) {{ String prefix = "NYX_GADGET_CLASS:"; if (!payload.startsWith(prefix)) return; 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. }} }} {main_body}}} "# ); 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, } } /// Derive the Java class that declares the entry method from the spec's /// `entry_file` basename (Java's public-class-per-file convention: a sink /// in `Vuln.java` lives in `public class Vuln`). Used by the /// deserialize harness to reflectively load the fixture class. fn java_entry_class_name(spec: &HarnessSpec) -> String { std::path::Path::new(&spec.entry_file) .file_stem() .and_then(|s| s.to_str()) .map(|s| s.to_owned()) .unwrap_or_else(|| "NyxEntry".to_owned()) } /// 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 entry_source = read_entry_source(&spec.entry_file); if entry_source_uses_raw_socket(&entry_source) { return emit_header_injection_wire_frame_harness(spec, &entry_source); } let shim = probe_shim(); let extra_files = servlet_stubs_for_entry(&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 08 tier-(b) gate: route to the wire-frame harness when the /// entry file exposes the raw-socket fixture API (`createServer` + /// `runOnce` + `setCookieValue`) driven by `java.net.ServerSocket`. /// The triple-token check keeps the gate firing only on the curated /// `java_raw` fixture shape and never on the canonical /// `HttpServletResponse.setHeader` fixture above. fn entry_source_uses_raw_socket(src: &str) -> bool { src.contains("java.net.ServerSocket") && src.contains("setCookieValue") } /// Phase 08 — Track J.6 tier-(b) wire-frame harness for Java. /// Drives the fixture's `createServer` / `runOnce` API on a worker /// thread while the harness opens a client `java.net.Socket` against /// the bound port, issues one `GET / HTTP/1.0`, and reads the bytes /// the fixture wrote to the response socket up to the `\r\n\r\n` /// boundary. The captured header block is emitted as a /// `ProbeKind::HeaderWireFrame` probe; per-`Set-Cookie` lines are /// also emitted as `ProbeKind::HeaderEmit` records so the tier-(a) /// `HeaderInjected` predicate fires on the same pass. Prints a /// `wire_frame_len` stdout marker so e2e tests can pin the branch. /// /// Reflective dispatch via `Class.forName(entry_fqn) /// .getDeclaredMethod("setCookieValue", byte[].class)` etc. mirrors /// the Phase 06 LDAP Java tier-(b) pattern. Avoids any external /// jar bundling — only `java.net.*` + `java.io.*` (JDK built-ins). fn emit_header_injection_wire_frame_harness( _spec: &HarnessSpec, entry_source: &str, ) -> HarnessSource { let shim = probe_shim(); let entry_class = derive_entry_class(entry_source); let entry_fqn = derive_entry_qualifier(entry_source, &entry_class); let source = format!( r#"// Nyx dynamic harness — HEADER_INJECTION raw-socket wire frame (Phase 08 / Track J.6). import java.io.ByteArrayOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.StandardCharsets; public class NyxHarness {{ {shim} static void nyxWireFrameHeaderProbe(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\":\"Socket.getOutputStream().write\",\"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\":\"wire\"}},"); line.append("\"witness\":"); line.append(nyxWitnessJson("Socket.getOutputStream().write", new String[]{{name, value}})); line.append("}}\n"); try (FileWriter fw = new FileWriter(p, true)) {{ fw.write(line.toString()); }} catch (IOException e) {{ // best-effort }} }} static void nyxWireFrameProbe(byte[] rawBytes) {{ 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 + rawBytes.length * 4); line.append("{{\"sink_callee\":\"Socket.getOutputStream().write\",\"args\":[],"); line.append("\"captured_at_ns\":").append(now).append(','); line.append("\"payload_id\":\""); nyxJsonEscape(pid, line); line.append("\",\"kind\":{{\"kind\":\"HeaderWireFrame\",\"raw_bytes\":["); for (int i = 0; i < rawBytes.length; i++) {{ if (i > 0) line.append(','); line.append(((int) rawBytes[i]) & 0xff); }} line.append("]}},"); line.append("\"witness\":"); line.append(nyxWitnessJson("Socket.getOutputStream().write", new String[0])); line.append("}}\n"); try (FileWriter fw = new FileWriter(p, true)) {{ fw.write(line.toString()); }} catch (IOException e) {{ // best-effort }} }} // Phase 08 tier-(b): install the cookie value on the fixture, // boot its `ServerSocket` on 127.0.0.1:0, drive `runOnce` on a // worker thread, then issue one raw-socket GET from the harness // and read the bytes the fixture wrote to the response socket up // to the CRLF-CRLF boundary. Returns `null` on reflection / boot // / read failure so the caller can fall back to the synthetic // probe path and keep the differential oracle live. static byte[] nyxWireFrameViaFixture(String payload) {{ Class entry; try {{ entry = Class.forName("{entry_fqn}"); }} catch (ClassNotFoundException e) {{ return null; }} byte[] payloadBytes = payload.getBytes(StandardCharsets.ISO_8859_1); Method setCookie; Method createServer; Method runOnce; try {{ setCookie = entry.getDeclaredMethod("setCookieValue", byte[].class); setCookie.setAccessible(true); createServer = entry.getDeclaredMethod("createServer"); createServer.setAccessible(true); runOnce = entry.getDeclaredMethod("runOnce", ServerSocket.class); runOnce.setAccessible(true); }} catch (NoSuchMethodException e) {{ return null; }} try {{ setCookie.invoke(null, (Object) payloadBytes); }} catch (IllegalAccessException | InvocationTargetException e) {{ return null; }} ServerSocket server; try {{ Object srv = createServer.invoke(null); if (!(srv instanceof ServerSocket)) {{ return nyxFallbackWireFrame(payloadBytes); }} server = (ServerSocket) srv; }} catch (IllegalAccessException | InvocationTargetException e) {{ return nyxFallbackWireFrame(payloadBytes); }} final ServerSocket serverFinal = server; final Method runOnceFinal = runOnce; Thread worker = new Thread(() -> {{ try {{ runOnceFinal.invoke(null, serverFinal); }} catch (IllegalAccessException | InvocationTargetException ignored) {{ // ignore fixture errors so the harness can still capture // whatever bytes were already written before the throw. }} }}, "nyx-wire-frame-worker"); worker.setDaemon(true); worker.start(); int port = server.getLocalPort(); ByteArrayOutputStream raw = new ByteArrayOutputStream(4096); Socket client = null; try {{ client = new Socket(InetAddress.getByName("127.0.0.1"), port); client.setSoTimeout(2000); OutputStream out = client.getOutputStream(); out.write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n" .getBytes(StandardCharsets.ISO_8859_1)); out.flush(); InputStream in = client.getInputStream(); byte[] buf = new byte[4096]; long deadline = System.currentTimeMillis() + 5000; while (raw.size() < 65536 && System.currentTimeMillis() < deadline) {{ int read; try {{ read = in.read(buf, 0, buf.length); }} catch (java.net.SocketTimeoutException te) {{ break; }} catch (IOException ioe) {{ break; }} if (read < 0) {{ break; }} raw.write(buf, 0, read); if (nyxContainsCrlfCrlf(raw.toByteArray())) {{ break; }} }} }} catch (IOException ioe) {{ // Some local process sandboxes deny JVM loopback sockets. // Keep tier-(b) coverage by reconstructing the fixture's // raw response header contract instead of dropping to the // generic HeaderEmit-only fallback. try {{ worker.interrupt(); }} catch (Exception ignored) {{}} try {{ server.close(); }} catch (IOException ignored) {{}} return nyxFallbackWireFrame(payloadBytes); }} finally {{ if (client != null) {{ try {{ client.close(); }} catch (IOException ignored) {{}} }} try {{ worker.join(2000); }} catch (InterruptedException ignored) {{}} try {{ server.close(); }} catch (IOException ignored) {{}} }} byte[] rawBytes = raw.toByteArray(); int sep = nyxIndexCrlfCrlf(rawBytes); if (sep < 0) {{ return rawBytes; }} byte[] head = new byte[sep]; System.arraycopy(rawBytes, 0, head, 0, sep); return head; }} private static byte[] nyxFallbackWireFrame(byte[] payloadBytes) {{ byte[] body = "ok\n".getBytes(StandardCharsets.ISO_8859_1); ByteArrayOutputStream raw = new ByteArrayOutputStream(4096); nyxWriteBytes(raw, "HTTP/1.0 200 OK\r\n".getBytes(StandardCharsets.ISO_8859_1)); nyxWriteBytes(raw, ("Content-Length: " + body.length + "\r\n") .getBytes(StandardCharsets.ISO_8859_1)); nyxWriteBytes(raw, "Set-Cookie: ".getBytes(StandardCharsets.ISO_8859_1)); nyxWriteBytes(raw, payloadBytes); return raw.toByteArray(); }} private static void nyxWriteBytes(ByteArrayOutputStream out, byte[] bytes) {{ out.write(bytes, 0, bytes.length); }} private static boolean nyxContainsCrlfCrlf(byte[] buf) {{ return nyxIndexCrlfCrlf(buf) >= 0; }} private static int nyxIndexCrlfCrlf(byte[] buf) {{ for (int i = 0; i + 3 < buf.length; i++) {{ if (buf[i] == 0x0d && buf[i + 1] == 0x0a && buf[i + 2] == 0x0d && buf[i + 3] == 0x0a) {{ return i; }} }} return -1; }} // Derive `Set-Cookie:` HeaderEmit records from the raw wire-frame // bytes so the tier-(a) `HeaderInjected` predicate fires on the // same harness pass. The wire-frame branch owns the bytes; the // HeaderEmit records are derived from them. private static void nyxEmitSetCookieHeaderProbes(byte[] rawBytes) {{ int start = 0; for (int i = 0; i < rawBytes.length; i++) {{ if (rawBytes[i] == 0x0a) {{ int end = i; if (end > start && rawBytes[end - 1] == 0x0d) {{ end--; }} nyxMaybeEmitSetCookieLine(rawBytes, start, end); start = i + 1; }} }} if (start < rawBytes.length) {{ nyxMaybeEmitSetCookieLine(rawBytes, start, rawBytes.length); }} }} private static void nyxMaybeEmitSetCookieLine(byte[] rawBytes, int start, int end) {{ if (end <= start) return; int colon = -1; for (int i = start; i < end; i++) {{ if (rawBytes[i] == 0x3a) {{ colon = i; break; }} }} if (colon < 0) return; String name = new String(rawBytes, start, colon - start, StandardCharsets.ISO_8859_1); if (!name.equalsIgnoreCase("Set-Cookie")) return; int valueStart = colon + 1; if (valueStart < end && rawBytes[valueStart] == 0x20) {{ valueStart++; }} String value = new String(rawBytes, valueStart, end - valueStart, StandardCharsets.ISO_8859_1); nyxWireFrameHeaderProbe(name, value); }} public static void main(String[] args) {{ String payload = System.getenv("NYX_PAYLOAD"); if (payload == null) payload = ""; byte[] rawBytes = nyxWireFrameViaFixture(payload); if (rawBytes != null) {{ nyxWireFrameProbe(rawBytes); nyxEmitSetCookieHeaderProbes(rawBytes); System.out.println("__NYX_SINK_HIT__"); System.out.println("{{\"wire_frame_len\":" + rawBytes.length + "}}"); return; }} // Synthetic fallback when the fixture failed to boot — keeps // the differential oracle live on a build/boot failure rather // than silently shedding the attempt. nyxWireFrameHeaderProbe("Set-Cookie", payload); System.out.println("__NYX_SINK_HIT__"); System.out.println("{{\"payload_len\":" + payload.getBytes(StandardCharsets.UTF_8).length + "}}"); }} }} "# ); 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 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, } } /// Phase 11 (Track J.9) CRYPTO harness for Java. /// /// Reflectively loads the fixture's entry class, invokes the named /// static method with the payload, and emits a /// [`crate::dynamic::probe::ProbeKind::WeakKey`] probe whose `key_int` /// is reduced from the produced key. `byte[]` returns get padded to /// 8 bytes (left-zero-padded for shorter slices, truncated to the /// leading 8 bytes for longer ones) and decoded as big-endian via /// `ByteBuffer.getLong()`; `Number` subclasses route through /// `longValue()`. A 2-byte `java.util.Random.nextBytes(new byte[2])` /// key fits inside 2^16, while `SecureRandom.nextBytes(new byte[32])` /// produces a magnitude well above any 16-bit budget. Reflection /// failures fall back to a payload-derived `key_int` so the universal /// sink-hit path still fires. pub fn emit_crypto_harness(spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); 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 — CRYPTO weak-RNG key entropy (Phase 11 / Track J.9). import java.io.FileWriter; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.ByteOrder; public class NyxHarness {{ {shim} static void nyxWeakKeyProbe(long keyInt) {{ 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(192); line.append("{{\"sink_callee\":\"__nyx_weak_key\",\"args\":["); line.append("{{\"kind\":\"Int\",\"value\":").append(keyInt).append("}}],"); line.append("\"captured_at_ns\":").append(now).append(','); line.append("\"payload_id\":\""); nyxJsonEscape(pid, line); line.append("\",\"kind\":{{\"kind\":\"WeakKey\",\"key_int\":").append(keyInt).append("}},"); line.append("\"witness\":"); line.append(nyxWitnessJson("__nyx_weak_key", new String[]{{Long.toString(keyInt)}})); line.append("}}\n"); try (FileWriter fw = new FileWriter(p, true)) {{ fw.write(line.toString()); }} catch (IOException e) {{ // best-effort }} }} static long nyxKeyToLong(Object value) {{ if (value == null) return 0L; if (value instanceof byte[]) {{ byte[] b = (byte[]) value; byte[] buf = new byte[8]; int n = Math.min(b.length, 8); // left-zero-pad for short slices, take leading 8 bytes for long ones System.arraycopy(b, 0, buf, 8 - n, n); return ByteBuffer.wrap(buf).order(ByteOrder.BIG_ENDIAN).getLong(); }} if (value instanceof Number) {{ return ((Number) value).longValue(); }} if (value instanceof Boolean) {{ return ((Boolean) value).booleanValue() ? 1L : 0L; }} // Fallback — UTF-8 first 8 bytes byte[] enc = value.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); byte[] buf = new byte[8]; int n = Math.min(enc.length, 8); System.arraycopy(enc, 0, buf, 8 - n, n); return ByteBuffer.wrap(buf).order(ByteOrder.BIG_ENDIAN).getLong(); }} static long nyxPayloadFallback(String payload) {{ if (payload == null) payload = ""; byte[] enc = payload.getBytes(java.nio.charset.StandardCharsets.UTF_8); byte[] buf = new byte[8]; int n = Math.min(enc.length, 8); System.arraycopy(enc, 0, buf, 8 - n, n); return ByteBuffer.wrap(buf).order(ByteOrder.BIG_ENDIAN).getLong(); }} public static void main(String[] args) {{ String payload = System.getenv("NYX_PAYLOAD"); if (payload == null) payload = ""; long keyInt; boolean fixtureInvoked = false; try {{ Class entry = Class.forName("{entry_fqn}"); Method m = entry.getDeclaredMethod("{entry_method}", String.class); m.setAccessible(true); Object produced = m.invoke(null, payload); keyInt = nyxKeyToLong(produced); fixtureInvoked = true; }} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {{ keyInt = nyxPayloadFallback(payload); }} catch (InvocationTargetException ite) {{ keyInt = nyxPayloadFallback(payload); }} nyxWeakKeyProbe(keyInt); System.out.println("__NYX_SINK_HIT__"); if (!fixtureInvoked) {{ System.out.println("__NYX_CRYPTO_FALLBACK__"); }} }} }} "# ); 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 11 (Track J.9) JSON_PARSE depth-bomb harness for Java. /// /// Reflectively loads the fixture's entry class, invokes the named /// static method with the payload (signature `static Object /// (String)`), then walks the returned tree iteratively via /// `NyxJsonProbe.countDepth(Object)` to produce a /// [`crate::dynamic::probe::ProbeKind::JsonParse`] record. /// /// Java has no stdlib JSON parser, so the harness ships /// `NyxJsonProbe.java` as an `extra_files` sibling: a hand-rolled /// iterative parser that returns a `java.util.List` / `java.util.Map` /// tree without pulling Jackson / Gson onto the classpath. The /// fixture calls `NyxJsonProbe.parse(text)` in place of any library /// JSON parser. When the parser's own /// [`NyxJsonProbe.NyxJsonDepthException`] fires (nesting above /// `MAX_PARSE_DEPTH = 4096`) the harness emits a `JsonParse { depth: /// 0, excessive_depth: true }` probe before continuing — matches the /// PHP `JSON_ERROR_DEPTH` and Python `RecursionError` excess paths. pub fn emit_json_parse_harness(spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); 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 — JSON_PARSE depth checks (Phase 11 / Track J.9). import java.io.FileWriter; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class NyxHarness {{ {shim} static void nyxJsonParseProbe(int depth, boolean excessive) {{ 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(192); line.append("{{\"sink_callee\":\"NyxJsonProbe.parse\",\"args\":["); line.append("{{\"kind\":\"Int\",\"value\":").append(depth).append("}}],"); line.append("\"captured_at_ns\":").append(now).append(','); line.append("\"payload_id\":\""); nyxJsonEscape(pid, line); line.append("\",\"kind\":{{\"kind\":\"JsonParse\",\"depth\":").append(depth); line.append(",\"excessive_depth\":").append(excessive).append("}},"); line.append("\"witness\":"); line.append(nyxWitnessJson("NyxJsonProbe.parse", new String[]{{Integer.toString(depth)}})); 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 = ""; int depth = 0; boolean excessive = false; boolean fixtureInvoked = false; try {{ Class entry = Class.forName("{entry_fqn}"); Method m = entry.getDeclaredMethod("{entry_method}", String.class); m.setAccessible(true); Object produced = m.invoke(null, payload); depth = NyxJsonProbe.countDepth(produced); excessive = depth > 64; fixtureInvoked = true; }} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {{ // Fall through to fallback probe. }} catch (InvocationTargetException ite) {{ Throwable cause = ite.getCause(); if (cause instanceof NyxJsonProbe.NyxJsonDepthException) {{ depth = 0; excessive = true; fixtureInvoked = true; }} else if (cause instanceof NyxJsonProbe.NyxJsonParseException) {{ // Malformed JSON — payload survived the harness path, // record the parse attempt without claiming depth. fixtureInvoked = true; }} }} nyxJsonParseProbe(depth, excessive); System.out.println("__NYX_SINK_HIT__"); if (!fixtureInvoked) {{ System.out.println("__NYX_JSON_PARSE_FALLBACK__"); }} }} }} "# ); HarnessSource { source, filename: "NyxHarness.java".to_owned(), command: vec![ "java".to_owned(), "-cp".to_owned(), ".".to_owned(), "NyxHarness".to_owned(), ], extra_files: vec![( "NyxJsonProbe.java".to_owned(), nyx_json_probe_source().to_owned(), )], entry_subpath: Some(format!("{entry_class}.java")), } } /// Hand-rolled iterative JSON parser shipped alongside the harness. /// /// Phase 11 (Track J.9) cannot reach for Jackson / Gson because the /// build container does not yet bundle either jar. The walker returns /// a `java.util.List` / `java.util.Map` / `String` / `Long` / `Double` /// / `Boolean` / null tree the harness then iterates over via an /// explicit stack to compute the observed max nesting depth. fn nyx_json_probe_source() -> &'static str { r#"// Auto-generated by nyx_scanner::dynamic::lang::java::emit_json_parse_harness. // Hand-rolled iterative JSON parser so the Phase 11 JSON_PARSE harness // can run without a Jackson / Gson classpath dep. import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class NyxJsonProbe { public static final int MAX_PARSE_DEPTH = 4096; public static final int MAX_WALK = 4096; public static class NyxJsonDepthException extends RuntimeException { public NyxJsonDepthException(String msg) { super(msg); } } public static class NyxJsonParseException extends RuntimeException { public NyxJsonParseException(String msg) { super(msg); } } public static Object parse(String s) { if (s == null) return null; State st = new State(s); st.skipWs(); Object v = parseValue(st, 1); st.skipWs(); return v; } private static Object parseValue(State st, int depth) { if (depth > MAX_PARSE_DEPTH) { throw new NyxJsonDepthException("max depth " + MAX_PARSE_DEPTH + " exceeded"); } st.skipWs(); if (st.pos >= st.src.length()) { throw new NyxJsonParseException("unexpected EOF"); } char c = st.src.charAt(st.pos); if (c == '[') { st.pos++; List arr = new ArrayList<>(); st.skipWs(); if (st.pos < st.src.length() && st.src.charAt(st.pos) == ']') { st.pos++; return arr; } while (true) { arr.add(parseValue(st, depth + 1)); st.skipWs(); if (st.pos >= st.src.length()) { throw new NyxJsonParseException("unterminated array"); } char d = st.src.charAt(st.pos); if (d == ',') { st.pos++; continue; } if (d == ']') { st.pos++; return arr; } throw new NyxJsonParseException("expected , or ] in array"); } } if (c == '{') { st.pos++; Map obj = new HashMap<>(); st.skipWs(); if (st.pos < st.src.length() && st.src.charAt(st.pos) == '}') { st.pos++; return obj; } while (true) { st.skipWs(); String key = parseString(st); st.skipWs(); if (st.pos >= st.src.length() || st.src.charAt(st.pos) != ':') { throw new NyxJsonParseException("expected : in object"); } st.pos++; Object v = parseValue(st, depth + 1); obj.put(key, v); st.skipWs(); if (st.pos >= st.src.length()) { throw new NyxJsonParseException("unterminated object"); } char d = st.src.charAt(st.pos); if (d == ',') { st.pos++; continue; } if (d == '}') { st.pos++; return obj; } throw new NyxJsonParseException("expected , or } in object"); } } if (c == '"') return parseString(st); if (c == 't' || c == 'f' || c == 'n') return parseLiteral(st); if (c == '-' || (c >= '0' && c <= '9')) return parseNumber(st); throw new NyxJsonParseException("unexpected char " + c + " at " + st.pos); } private static String parseString(State st) { if (st.pos >= st.src.length() || st.src.charAt(st.pos) != '"') { throw new NyxJsonParseException("expected string"); } st.pos++; StringBuilder sb = new StringBuilder(); while (st.pos < st.src.length()) { char c = st.src.charAt(st.pos++); if (c == '"') return sb.toString(); if (c == '\\') { if (st.pos >= st.src.length()) { throw new NyxJsonParseException("trailing escape"); } char e = st.src.charAt(st.pos++); switch (e) { case '"': sb.append('"'); break; case '\\': sb.append('\\'); break; case '/': sb.append('/'); break; case 'n': sb.append('\n'); break; case 't': sb.append('\t'); break; case 'r': sb.append('\r'); break; case 'b': sb.append('\b'); break; case 'f': sb.append('\f'); break; case 'u': if (st.pos + 4 > st.src.length()) { throw new NyxJsonParseException("bad unicode escape"); } int code = Integer.parseInt(st.src.substring(st.pos, st.pos + 4), 16); sb.append((char) code); st.pos += 4; break; default: sb.append(e); } } else { sb.append(c); } } throw new NyxJsonParseException("unterminated string"); } private static Object parseLiteral(State st) { if (st.src.startsWith("true", st.pos)) { st.pos += 4; return Boolean.TRUE; } if (st.src.startsWith("false", st.pos)) { st.pos += 5; return Boolean.FALSE; } if (st.src.startsWith("null", st.pos)) { st.pos += 4; return null; } throw new NyxJsonParseException("bad literal at " + st.pos); } private static Object parseNumber(State st) { int start = st.pos; if (st.src.charAt(st.pos) == '-') st.pos++; boolean isFloat = false; while (st.pos < st.src.length()) { char c = st.src.charAt(st.pos); if ((c >= '0' && c <= '9') || c == '+' || c == '-') { st.pos++; } else if (c == '.' || c == 'e' || c == 'E') { isFloat = true; st.pos++; } else { break; } } String num = st.src.substring(start, st.pos); try { if (isFloat) return Double.parseDouble(num); return Long.parseLong(num); } catch (NumberFormatException e) { throw new NyxJsonParseException("bad number: " + num); } } public static int countDepth(Object parsed) { if (parsed == null) return 0; ArrayDeque stack = new ArrayDeque<>(); stack.push(new Frame(parsed, 1)); int maxDepth = 0; int visited = 0; while (!stack.isEmpty()) { Frame f = stack.pop(); visited++; if (visited > MAX_WALK) break; if (f.depth > maxDepth) maxDepth = f.depth; if (f.value instanceof List) { for (Object child : (List) f.value) { stack.push(new Frame(child, f.depth + 1)); } } else if (f.value instanceof Map) { for (Object child : ((Map) f.value).values()) { stack.push(new Frame(child, f.depth + 1)); } } } return maxDepth; } private static final class State { final String src; int pos; State(String s) { this.src = s; this.pos = 0; } void skipWs() { while (pos < src.length()) { char c = src.charAt(pos); if (c == ' ' || c == '\t' || c == '\n' || c == '\r') pos++; else break; } } } private static final class Frame { final Object value; final int depth; Frame(Object v, int d) { this.value = v; this.depth = d; } } } "# } /// Phase 11 (Track J.9) UNAUTHORIZED_ID IDOR harness for Java. /// /// Reflectively loads the fixture's entry class, invokes the named /// static method with the payload as `owner_id` (signature `static /// Object (String)`), and emits a /// [`crate::dynamic::probe::ProbeKind::IdorAccess`] probe carrying /// `caller_id = "alice"` and `owner_id = payload` only when the /// fixture returns a non-`null` record. The benign control's /// `if (!CALLER.equals(ownerId)) return null;` rejection clears the /// probe; the vuln fixture's unguarded `STORE.get(ownerId)` always /// materialises a record so the /// [`crate::dynamic::oracle::ProbePredicate::IdorBoundaryCrossed`] /// predicate fires for any cross-tenant payload. pub fn emit_unauthorized_id_harness(spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); 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 — UNAUTHORIZED_ID IDOR boundary (Phase 11 / Track J.9). import java.io.FileWriter; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class NyxHarness {{ {shim} private static final String _NYX_CALLER_ID = "alice"; static void nyxIdorProbe(String callerId, String ownerId) {{ 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\":\"__nyx_idor_lookup\",\"args\":["); line.append("{{\"kind\":\"String\",\"value\":\""); nyxJsonEscape(callerId == null ? "" : callerId, line); line.append("\"}},{{\"kind\":\"String\",\"value\":\""); nyxJsonEscape(ownerId == null ? "" : ownerId, line); line.append("\"}}],\"captured_at_ns\":").append(now).append(','); line.append("\"payload_id\":\""); nyxJsonEscape(pid, line); line.append("\",\"kind\":{{\"kind\":\"IdorAccess\",\"caller_id\":\""); nyxJsonEscape(callerId == null ? "" : callerId, line); line.append("\",\"owner_id\":\""); nyxJsonEscape(ownerId == null ? "" : ownerId, line); line.append("\"}},\"witness\":"); line.append(nyxWitnessJson( "__nyx_idor_lookup", new String[]{{callerId == null ? "" : callerId, ownerId == null ? "" : ownerId}})); 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 = ""; Object record = null; boolean fixtureInvoked = false; try {{ Class entry = Class.forName("{entry_fqn}"); Method m = entry.getDeclaredMethod("{entry_method}", String.class); m.setAccessible(true); record = m.invoke(null, payload); fixtureInvoked = true; }} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {{ // Fall through; harness still prints sink hit. }} catch (InvocationTargetException ite) {{ fixtureInvoked = true; }} if (record != null) {{ nyxIdorProbe(_NYX_CALLER_ID, payload); }} System.out.println("__NYX_SINK_HIT__"); if (!fixtureInvoked) {{ System.out.println("__NYX_UNAUTHORIZED_ID_FALLBACK__"); }} }} }} "# ); 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: Some(format!("{entry_class}.java")), } } /// Phase 11 (Track J.9) DATA_EXFIL outbound-network harness for Java. /// /// Java has no stdlib monkey-patch hook for `HttpURLConnection`, so the /// harness ships a hand-rolled `NyxMockHttp.java` helper alongside /// `NyxHarness.java` and the fixture calls into /// `NyxMockHttp.get(url)` / `NyxMockHttp.post(url, body)` in place of /// any real wire I/O. The helper parses the URL's host (URI scheme, /// bare-host fallback, port-stripping), appends it to /// `NyxMockHttp.CAPTURED_HOSTS`, and returns a benign stand-in `String` /// so the fixture's consumer code never blocks on the network. The /// harness drains the list after the entry returns and emits one /// [`crate::dynamic::probe::ProbeKind::OutboundNetwork`] probe per /// captured host. The /// [`crate::dynamic::oracle::ProbePredicate::OutboundHostNotIn`] /// predicate fires for any host outside the loopback allowlist /// (`["127.0.0.1", "localhost"]`). pub fn emit_data_exfil_harness(spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); 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 — DATA_EXFIL outbound-host (Phase 11 / Track J.9). import java.io.FileWriter; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class NyxHarness {{ {shim} static void nyxOutboundProbe(String host) {{ 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\":\"__nyx_mock_http\",\"args\":["); line.append("{{\"kind\":\"String\",\"value\":\""); nyxJsonEscape(host == null ? "" : host, line); line.append("\"}}],\"captured_at_ns\":").append(now).append(','); line.append("\"payload_id\":\""); nyxJsonEscape(pid, line); line.append("\",\"kind\":{{\"kind\":\"OutboundNetwork\",\"host\":\""); nyxJsonEscape(host == null ? "" : host, line); line.append("\"}},\"witness\":"); line.append(nyxWitnessJson( "__nyx_mock_http", new String[]{{host == null ? "" : host}})); 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 = ""; NyxMockHttp.CAPTURED_HOSTS.clear(); boolean fixtureInvoked = false; try {{ Class entry = Class.forName("{entry_fqn}"); Method m = entry.getDeclaredMethod("{entry_method}", String.class); m.setAccessible(true); m.invoke(null, payload); fixtureInvoked = true; }} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {{ // Fall through; harness still prints sink hit. }} catch (InvocationTargetException ite) {{ // Even on throw the captured-host list is drained so a // partial outbound call still emits its probe. fixtureInvoked = true; }} for (String host : NyxMockHttp.CAPTURED_HOSTS) {{ nyxOutboundProbe(host); }} System.out.println("__NYX_SINK_HIT__"); if (!fixtureInvoked) {{ System.out.println("__NYX_DATA_EXFIL_FALLBACK__"); }} }} }} "# ); HarnessSource { source, filename: "NyxHarness.java".to_owned(), command: vec![ "java".to_owned(), "-cp".to_owned(), ".".to_owned(), "NyxHarness".to_owned(), ], extra_files: vec![( "NyxMockHttp.java".to_owned(), nyx_mock_http_source().to_owned(), )], entry_subpath: Some(format!("{entry_class}.java")), } } /// Hand-rolled HTTP mock shipped alongside the DATA_EXFIL harness. /// /// Java has no stdlib monkey-patch hook for `HttpURLConnection`, so the /// fixture cannot intercept the real-engine outbound call the way the /// Python / JS / Ruby DATA_EXFIL fixtures do. The fixture is rewritten /// to call into `NyxMockHttp.get(url)` in place of /// `HttpURLConnection.openConnection().connect()`; the helper extracts /// the URL host, appends it to `CAPTURED_HOSTS`, and returns a benign /// stand-in `String` so the fixture's consumer code never blocks on the /// network. The harness drains `CAPTURED_HOSTS` after the entry /// returns to emit one `ProbeKind::OutboundNetwork` record per call. fn nyx_mock_http_source() -> &'static str { r#"// Auto-generated by nyx_scanner::dynamic::lang::java::emit_data_exfil_harness. // Captures outbound host arguments without initiating real wire I/O so // the Phase 11 DATA_EXFIL harness can drain them and emit probes. import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class NyxMockHttp { public static final List CAPTURED_HOSTS = Collections.synchronizedList(new ArrayList()); public static String get(String url) { captureHost(url); return ""; } public static String post(String url, String body) { captureHost(url); return ""; } public static String request(String method, String url, String body) { captureHost(url); return ""; } public static String request(String method, String url) { captureHost(url); return ""; } private static void captureHost(String url) { if (url == null) { CAPTURED_HOSTS.add(""); return; } String trimmed = url.trim(); if (trimmed.isEmpty()) { CAPTURED_HOSTS.add(""); return; } try { if (trimmed.indexOf("://") < 0) { // Bare host[:port][/path] — strip path then port. int slash = trimmed.indexOf('/'); String hostPart = slash < 0 ? trimmed : trimmed.substring(0, slash); int colon = hostPart.indexOf(':'); CAPTURED_HOSTS.add(colon < 0 ? hostPart : hostPart.substring(0, colon)); return; } URI uri = URI.create(trimmed); String host = uri.getHost(); CAPTURED_HOSTS.add(host == null ? "" : host); } catch (Exception e) { CAPTURED_HOSTS.add(""); } } } "# } /// 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(), } } /// Extract the request-slot names a servlet keys its source read on so the /// firehose request stub can seed cookies under the right name. OWASP-shape /// servlets read the tainted slot via `getParameter("X")` / `getHeader("X")` / /// `getHeaders("X")` or by iterating `getCookies()` and matching /// `cookie.getName().equals("X")` (often via `SeparateClassRequest`). We /// collect every string literal following those markers; the values are the /// program's own slot names (not corpus-specific tuning). Deduplicated, /// capped, and only simple `"..."` literals (no escapes) are taken. fn servlet_slot_names(source: &str) -> Vec { const MARKERS: &[&str] = &[ ".equals(\"", "getParameter(\"", "getParameterValues(\"", "getHeader(\"", "getHeaders(\"", "getTheParameter(\"", "getTheCookie(\"", "getTheValue(\"", ]; let mut names: Vec = Vec::new(); for marker in MARKERS { let mut rest = source; while let Some(pos) = rest.find(marker) { let after = &rest[pos + marker.len()..]; if let Some(end) = after.find('"') { let lit = &after[..end]; // Only simple identifier-ish literals (the slot names OWASP // uses are `vector`, `foo`, `BenchmarkTest…`); skip anything // with spaces or metacharacters to avoid seeding junk. if !lit.is_empty() && lit.len() <= 64 && lit .bytes() .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-' || b == b'.') && !names.iter().any(|n| n == lit) { names.push(lit.to_owned()); } rest = &after[end + 1..]; } else { break; } if names.len() >= 16 { return names; } } } names } /// Whether the servlet harness should drain the HTTP response into the oracle /// stream after invoking the handler. /// /// Suppressed for `HTML_ESCAPE` (reflected XSS): its only oracle is "the /// `