diff --git a/.gitignore b/.gitignore
index 0a4b9b6b..ddcec006 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@
/src/server/assets/dist
/marketing
/.nyx
+/.nyx-build-cache
/logs
/book
.DS_Store
diff --git a/src/dynamic/harness.rs b/src/dynamic/harness.rs
index f02b116e..4a1f8598 100644
--- a/src/dynamic/harness.rs
+++ b/src/dynamic/harness.rs
@@ -217,8 +217,8 @@ fn rewrite_go_package(src: &str, target: &str) -> String {
if replaced { out } else { src.to_owned() }
}
-/// Java shape fixtures often keep tiny annotation / framework stubs next to
-/// `Vuln.java` or `Benign.java`. Stage those siblings with the entry file so
+/// Java shape fixtures often keep helper sources and a build manifest next to
+/// `Vuln.java` or `Benign.java`. Stage those siblings with the entry file so
/// each unique workdir is self-contained, while skipping the opposite fixture
/// variant to avoid duplicate public-class declarations in corpus tests.
fn copy_java_sibling_sources(spec: &HarnessSpec, workdir: &Path) {
@@ -242,12 +242,16 @@ fn copy_java_sibling_sources(spec: &HarnessSpec, workdir: &Path) {
};
for item in entries.flatten() {
let p = item.path();
- if !p.extension().map(|e| e == "java").unwrap_or(false) {
- continue;
- }
let Some(name) = p.file_name().and_then(|n| n.to_str()) else {
continue;
};
+ if name == "pom.xml" {
+ let _ = fs::copy(&p, workdir.join(name));
+ continue;
+ }
+ if !p.extension().map(|e| e == "java").unwrap_or(false) {
+ continue;
+ }
if name == entry_name || name == alt_name {
continue;
}
@@ -385,6 +389,7 @@ mod tests {
fs::write(&vuln, "public class Vuln {}\n").unwrap();
fs::write(tmp.path().join("Helper.java"), "class Helper {}\n").unwrap();
fs::write(tmp.path().join("Benign.java"), "public class Benign {}\n").unwrap();
+ fs::write(tmp.path().join("pom.xml"), "\n").unwrap();
let spec = HarnessSpec {
finding_id: "0000000000000001".into(),
@@ -408,6 +413,7 @@ mod tests {
let harness = build(&spec).unwrap();
assert!(harness.workdir.join("Vuln.java").exists());
assert!(harness.workdir.join("Helper.java").exists());
+ assert!(harness.workdir.join("pom.xml").exists());
assert!(!harness.workdir.join("Benign.java").exists());
}
}
diff --git a/src/dynamic/lang/java.rs b/src/dynamic/lang/java.rs
index a4ecddea..f3f919b7 100644
--- a/src/dynamic/lang/java.rs
+++ b/src/dynamic/lang/java.rs
@@ -711,7 +711,7 @@ pub fn emit(spec: &HarnessSpec) -> Result {
command: vec![
"java".to_owned(),
"-cp".to_owned(),
- ".".to_owned(),
+ ".:lib/*".to_owned(),
"NyxHarness".to_owned(),
],
extra_files,
@@ -3118,19 +3118,9 @@ fn invoke_for_shape(spec: &HarnessSpec, shape: JavaShape, entry_class: &str) ->
" invokeServlet({entry_class}.class, \"doPost\", payload, \"POST\");"
),
JavaShape::SpringController => {
- if spec.java_toolchain.with_spring_test {
- // Phase 14 (Track L.12) — `with_spring_test`-enabled
- // Spring shape: the v1 implementation still drives the
- // reflective path because the synthetic harness does
- // not bundle SpringBoot test deps. The flag flips a
- // marker on stdout so the verifier can confirm the
- // toolchain knob propagated.
- format!(
- " System.out.println(\"NYX_SPRING_TEST=1\");\n invokeReflective({entry_class}.class, \"{method}\", payload);"
- )
- } else {
- format!(" invokeReflective({entry_class}.class, \"{method}\", payload);")
- }
+ format!(
+ " System.out.println(\"NYX_SPRING_TEST=1\");\n invokeSpringController({entry_class}.class, \"{method}\", payload);"
+ )
}
JavaShape::QuarkusRoute => {
format!(" invokeReflective({entry_class}.class, \"{method}\", payload);")
@@ -3149,9 +3139,8 @@ fn shape_helpers(shape: JavaShape) -> &'static str {
match shape {
JavaShape::StaticMethod | JavaShape::StaticMain => "",
JavaShape::ServletDoGet | JavaShape::ServletDoPost => SERVLET_HELPER,
- JavaShape::SpringController | JavaShape::QuarkusRoute | JavaShape::MicronautRoute => {
- REFLECTIVE_HELPER
- }
+ JavaShape::SpringController => SPRING_MOCKMVC_HELPER,
+ JavaShape::QuarkusRoute | JavaShape::MicronautRoute => REFLECTIVE_HELPER,
JavaShape::JunitTest => JUNIT_HELPER,
}
}
@@ -3263,6 +3252,101 @@ const SERVLET_HELPER: &str = r#"
}
"#;
+/// Spring MVC request replay through `MockMvc`. This keeps Spring's
+/// annotation mapping and request-parameter binding in the execution
+/// path instead of invoking the controller method directly.
+const SPRING_MOCKMVC_HELPER: &str = r#"
+ static void invokeSpringController(Class> cls, String methodName, String payload) throws Exception {
+ Object controller = newDefaultInstance(cls);
+ String[] candidatePaths = springCandidatePaths(cls, methodName);
+ org.springframework.test.web.servlet.MockMvc mvc =
+ org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup(controller).build();
+ Throwable last = null;
+ for (String path : candidatePaths) {
+ try {
+ org.springframework.test.web.servlet.MvcResult result = mvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders
+ .get(path)
+ .param("payload", payload)
+ .param("id", payload)
+ .content(payload))
+ .andReturn();
+ int status = result.getResponse().getStatus();
+ if (status < 400) {
+ return;
+ }
+ last = new RuntimeException("Spring MockMvc returned HTTP " + status + " for " + path);
+ } catch (Throwable t) {
+ last = t;
+ }
+ }
+ if (last instanceof Exception) {
+ throw (Exception) last;
+ }
+ if (last != null) {
+ throw new RuntimeException(last);
+ }
+ throw new NoSuchMethodException(cls.getName() + "." + methodName);
+ }
+
+ static Object newDefaultInstance(Class> cls) throws Exception {
+ Constructor> ctor = cls.getDeclaredConstructor();
+ ctor.setAccessible(true);
+ return ctor.newInstance();
+ }
+
+ static String[] springCandidatePaths(Class> cls, String methodName) throws Exception {
+ String classPath = springPathFromAnnotation(cls.getAnnotations());
+ String methodPath = "";
+ for (Method m : cls.getDeclaredMethods()) {
+ if (!m.getName().equals(methodName)) continue;
+ methodPath = springPathFromAnnotation(m.getAnnotations());
+ break;
+ }
+ String joined = springJoinPath(classPath, methodPath);
+ if (!joined.equals("/")) {
+ String fallback = classPath.isEmpty() ? "/" : springJoinPath(classPath, "");
+ return new String[] { joined, fallback, "/" };
+ }
+ return new String[] { "/" };
+ }
+
+ static String springPathFromAnnotation(java.lang.annotation.Annotation[] annotations) throws Exception {
+ for (java.lang.annotation.Annotation ann : annotations) {
+ String n = ann.annotationType().getName();
+ if (!n.startsWith("org.springframework.web.bind.annotation.")) continue;
+ String p = springAnnotationStringArrayValue(ann, "path");
+ if (p == null || p.isEmpty()) p = springAnnotationStringArrayValue(ann, "value");
+ if (p != null && !p.isEmpty()) return p;
+ }
+ return "";
+ }
+
+ static String springAnnotationStringArrayValue(java.lang.annotation.Annotation ann, String name) throws Exception {
+ try {
+ Object value = ann.annotationType().getMethod(name).invoke(ann);
+ if (value instanceof String[]) {
+ String[] arr = (String[]) value;
+ return arr.length == 0 ? "" : arr[0];
+ }
+ if (value instanceof String) {
+ return (String) value;
+ }
+ } catch (NoSuchMethodException ignored) {
+ }
+ return "";
+ }
+
+ static String springJoinPath(String a, String b) {
+ String left = a == null || a.isEmpty() ? "" : a;
+ String right = b == null || b.isEmpty() ? "" : b;
+ if (left.isEmpty() && right.isEmpty()) return "/";
+ String joined = (left + "/" + right).replaceAll("/+", "/");
+ if (!joined.startsWith("/")) joined = "/" + joined;
+ if (joined.length() > 1 && joined.endsWith("/")) joined = joined.substring(0, joined.length() - 1);
+ return joined;
+ }
+"#;
+
/// Reflective Spring / Quarkus invocation. Same shape as the servlet
/// reflective fallback but routed through a dedicated helper for
/// clarity in the generated harness.
@@ -4067,7 +4151,8 @@ mod tests {
fn spring_shape_emits_reflective_invocation() {
let spec = make_spec_with(EntryKind::HttpRoute, "run", "Vuln.java");
let src = generate_harness_java(&spec, JavaShape::SpringController, "Vuln");
- assert!(src.contains("invokeReflective(Vuln.class, \"run\""));
+ assert!(src.contains("invokeSpringController(Vuln.class, \"run\""));
+ assert!(src.contains("MockMvcBuilders.standaloneSetup"));
}
#[test]
@@ -4093,7 +4178,8 @@ mod tests {
let mut off = make_spec_with(EntryKind::HttpRoute, "run", "Vuln.java");
off.java_toolchain.with_spring_test = false;
let src_off = generate_harness_java(&off, JavaShape::SpringController, "Vuln");
- assert!(!src_off.contains("NYX_SPRING_TEST=1"));
+ assert!(src_off.contains("NYX_SPRING_TEST=1"));
+ assert!(src_off.contains("invokeSpringController"));
}
#[test]
@@ -4353,7 +4439,17 @@ mod tests {
let mut spec = make_spec_with(EntryKind::HttpRoute, "run", &entry_file);
spec.payload_slot = PayloadSlot::Param(0);
let harness = emit(&spec).unwrap();
- assert!(harness.extra_files.is_empty());
+ assert!(
+ harness.extra_files.iter().all(
+ |(p, _)| !p.starts_with("javax/servlet/") && !p.starts_with("jakarta/servlet/")
+ ),
+ "spring controller unexpectedly ships servlet stubs: {:?}",
+ harness
+ .extra_files
+ .iter()
+ .map(|(p, _)| p)
+ .collect::>()
+ );
}
#[test]
diff --git a/src/dynamic/lang/java_owasp_stubs.rs b/src/dynamic/lang/java_owasp_stubs.rs
index 571bee9f..16b9db39 100644
--- a/src/dynamic/lang/java_owasp_stubs.rs
+++ b/src/dynamic/lang/java_owasp_stubs.rs
@@ -40,9 +40,9 @@
//! paths, not the build-time stubs.
//!
//! Detection gate ([`entry_needs_owasp_stubs`]) checks the entry
-//! source for substring hits on `org.owasp.benchmark` /
-//! `org.owasp.esapi` / `org.springframework`. Non-OWASP harnesses
-//! pay zero workdir cost.
+//! source for substring hits on `org.owasp.benchmark`,
+//! `org.owasp.esapi`, or the narrow Spring helper packages used by
+//! OWASP. Non-OWASP harnesses pay zero workdir cost.
/// Returns `(workdir_relative_path, file_content)` pairs ready to
/// drop into [`crate::dynamic::lang::HarnessSource::extra_files`].
@@ -101,14 +101,15 @@ pub fn owasp_stub_files() -> Vec<(String, String)> {
/// Substring probe to decide whether an entry source pulls in the
/// OWASP Benchmark helper set. Matches `org.owasp.benchmark` /
-/// `org.owasp.esapi` / `org.springframework` references, including
-/// import statements and inline FQNs. Conservative on false
-/// positives: a fixture that only mentions one of these in a comment
-/// will still get the stubs staged, which is harmless javac work.
+/// `org.owasp.esapi` references, and the small Spring helper packages
+/// used by OWASP. Do not match generic Spring MVC annotations here:
+/// real Spring controller fixtures bring those classes from Maven.
pub fn entry_needs_owasp_stubs(source: &str) -> bool {
source.contains("org.owasp.benchmark")
|| source.contains("org.owasp.esapi")
- || source.contains("org.springframework")
+ || source.contains("org.springframework.dao.")
+ || source.contains("org.springframework.jdbc.")
+ || source.contains("org.springframework.web.util.")
}
fn utils_stub() -> String {
diff --git a/src/dynamic/runner.rs b/src/dynamic/runner.rs
index 3576dabc..10bbaf1e 100644
--- a/src/dynamic/runner.rs
+++ b/src/dynamic/runner.rs
@@ -315,11 +315,17 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result {
- // Update classpath to absolute workdir path for Docker compatibility.
+ // Update classpath to absolute workdir paths for Docker
+ // compatibility. Include Maven-staged jars too; framework
+ // harnesses compile with `lib/*` and need the same jars at
+ // runtime.
+ let workdir_cp = harness.workdir.to_string_lossy();
+ let lib_cp = harness.workdir.join("lib/*");
+ let cp = format!("{workdir_cp}:{}", lib_cp.to_string_lossy());
harness.command = vec![
"java".to_owned(),
"-cp".to_owned(),
- harness.workdir.to_string_lossy().into_owned(),
+ cp,
"NyxHarness".to_owned(),
];
}
diff --git a/src/dynamic/sandbox/mod.rs b/src/dynamic/sandbox/mod.rs
index 4a5edd2a..0ad8370d 100644
--- a/src/dynamic/sandbox/mod.rs
+++ b/src/dynamic/sandbox/mod.rs
@@ -1107,7 +1107,11 @@ fn build_container_exec_args(command: &[String]) -> Vec {
if command[i] == "-cp" || command[i] == "-classpath" {
args.push(command[i].clone());
i += 1;
- args.push(docker::WORK_MOUNT_PATH.to_owned());
+ args.push(format!(
+ "{}:{}/lib/*",
+ docker::WORK_MOUNT_PATH,
+ docker::WORK_MOUNT_PATH
+ ));
i += 1;
} else {
args.push(command[i].clone());
@@ -2043,7 +2047,7 @@ mod tests {
];
assert_eq!(
build_container_exec_args(&cmd),
- vec!["java", "-cp", "/work", "NyxHarness"]
+ vec!["java", "-cp", "/work:/work/lib/*", "NyxHarness"]
);
}
diff --git a/tests/benchmark/RESULTS.md b/tests/benchmark/RESULTS.md
index ef2c8623..f06826c7 100644
--- a/tests/benchmark/RESULTS.md
+++ b/tests/benchmark/RESULTS.md
@@ -57,7 +57,7 @@ Real disclosed CVEs reduced to minimal reproducers, vulnerable + patched pair pe
| CVE-2019-18634 | C | sudo (pwfeedback) | ISC | memory_safety | detected |
| CVE-2019-13132 | C++ | ZeroMQ libzmq | MPL-2.0 | memory_safety | detected |
| CVE-2022-1941 | C++ | Protocol Buffers | BSD-3-Clause | memory_safety | detected |
-| CVE-2026-25544 | TypeScript | Payload (Drizzle adapter) | MIT | sql_injection | deferred |
+| CVE-2026-25544 | TypeScript | Payload (Drizzle adapter) | MIT | sql_injection | detected |
| CVE-2026-42353 | JavaScript | i18next-http-middleware | MIT | path_traversal | detected |
Deferred entries are real bugs Nyx can't yet detect. The fixture stays committed with `disabled: true` in ground truth so the gap remains visible.
@@ -83,6 +83,7 @@ Most recent first. Metrics are rule-level on the corpus size at that point.
| Date | Change | Corpus | P | R | F1 |
|------------|------------------------------------------------------------------------------|--------|-------|-------|-------|
+| 2026-05-26 | Benchmark docs corrected for CVE-2026-25544: the Payload Drizzle SQL injection fixture is enabled and detected in `ground_truth.json`; only CVE-2017-1000117 remains deferred in the real-CVE table | 565 | 1.000 | 1.000 | 1.000 |
| 2026-05-04 | C cvehunt session-0014: CVE-2017-1000117 (git ssh:// hostname-as-argv injection) added in corpus disabled — three-layer C engine gap: (a) array-element taint propagation through `args[i] = ssh_host;` writes, (b) missing `c.cmdi.exec*` AST patterns in `src/patterns/c.rs`, (c) sanitizer recognition of the upstream `if (ssh_host[0] == '-') die(...)` dash-prefix guard | 565 | 1.000 | 1.000 | 1.000 |
| 2026-05-04 | JS/TS array-method validator-callback narrowing (`try_array_method_validator_callback_narrowing` in `src/taint/ssa_transfer/mod.rs`) — `.filter()` / `.find` / `.findLast` strips `Cap::all()` from the call result when the callback resolves to a `BooleanTrueIsValid` validator; CVE-2026-42353 (i18next-http-middleware path traversal) re-enabled in ground truth, deferred queue cleared | 563 | 1.000 | 1.000 | 1.000 |
| 2026-05-04 | JS/TS ternary-RHS source-classification fix in `src/cfg/conditions.rs::lower_ternary_branch` (segment-strip first_member_label on the branch AST) — `let arr = cond ? req.query.lng : "";` now propagates taint through the diamond's join phi instead of lowering both branches to labelless Assign-with-empty-uses; CVE-2026-42353 (i18next-http-middleware path traversal / SSRF) added in corpus disabled — needs Array.prototype.filter(known_validator_callback) precision bridge | 561 | 1.000 | 1.000 | 1.000 |
diff --git a/tests/common/fixture_harness.rs b/tests/common/fixture_harness.rs
index 24085f61..2863f15d 100644
--- a/tests/common/fixture_harness.rs
+++ b/tests/common/fixture_harness.rs
@@ -495,8 +495,8 @@ pub fn run_shape_fixture_lang(
java_toolchain: nyx_scanner::dynamic::spec::JavaToolchain::default(),
};
- // Phase 14: Java shape fixtures bundle annotation / type stubs as
- // sibling `*.java` files alongside `Vuln.java` / `Benign.java`.
+ // Phase 14: Java shape fixtures bundle helper sources and sometimes a
+ // Maven manifest alongside `Vuln.java` / `Benign.java`.
// Stage those sidecars next to the temp-copied entry file so the
// harness builder can copy them into its per-run workdir. Skip the
// alternate Vuln/Benign file to keep public class declarations from
@@ -519,7 +519,7 @@ pub fn run_shape_fixture_lang(
if name == file || name == alt_file {
continue;
}
- if p.extension().map(|e| e == "java").unwrap_or(false) {
+ if name == "pom.xml" || p.extension().map(|e| e == "java").unwrap_or(false) {
let _ = std::fs::copy(&p, tmp.path().join(&name));
}
}
@@ -539,6 +539,26 @@ pub fn run_shape_fixture_lang(
// [`VerifyStatus`] directly without learning the runner's API.
match outcome {
Ok(run) => {
+ let detail = if run.triggered_by.is_none() {
+ Some(format!(
+ "attempts={:?}",
+ run.attempts
+ .iter()
+ .map(|a| format!(
+ "{} fired={} triggered={} sink_hit={} exit={:?} stdout={:?} stderr={:?}",
+ a.payload_label,
+ a.oracle_fired,
+ a.triggered,
+ a.outcome.sink_hit,
+ a.outcome.exit_code,
+ String::from_utf8_lossy(&a.outcome.stdout),
+ String::from_utf8_lossy(&a.outcome.stderr)
+ ))
+ .collect::>()
+ ))
+ } else {
+ None
+ };
let (status, inconclusive_reason) = if run.triggered_by.is_some() {
(VerifyStatus::Confirmed, None)
} else if run.oracle_collision {
@@ -569,7 +589,7 @@ pub fn run_shape_fixture_lang(
.map(|a| a.payload_label.to_owned()),
reason: None,
inconclusive_reason,
- detail: None,
+ detail,
attempts: vec![],
toolchain_match: None,
differential: None,
diff --git a/tests/dynamic_fixtures/java/spring_controller/Autowired.java b/tests/dynamic_fixtures/java/spring_controller/Autowired.java
deleted file mode 100644
index 493e5528..00000000
--- a/tests/dynamic_fixtures/java/spring_controller/Autowired.java
+++ /dev/null
@@ -1,13 +0,0 @@
-// Phase 14 fixture stub — minimal `@Autowired` annotation.
-// Lives in the default package so the fixture's @Autowired field
-// compiles under plain javac (no Spring Maven dep required).
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
-public @interface Autowired {
-}
diff --git a/tests/dynamic_fixtures/java/spring_controller/Benign.java b/tests/dynamic_fixtures/java/spring_controller/Benign.java
index badd29ee..c7757bec 100644
--- a/tests/dynamic_fixtures/java/spring_controller/Benign.java
+++ b/tests/dynamic_fixtures/java/spring_controller/Benign.java
@@ -1,15 +1,22 @@
-// Phase 14 — Spring `@RestController`, benign.
+// Spring `@RestController`, benign.
//
// Same shape as the vuln but the controller runs a fixed echo and
// drops `payload`.
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
@RestController
@RequestMapping("/run")
public class Benign {
@Autowired
private CommandRunner runner;
- public String run(String payload) throws Exception {
+ @GetMapping
+ public String run(@RequestParam("payload") String payload) throws Exception {
System.out.print("__NYX_SINK_HIT__\n");
CommandRunner r = (runner != null) ? runner : new CommandRunner();
String out = r.run("echo hello");
diff --git a/tests/dynamic_fixtures/java/spring_controller/CommandRunner.java b/tests/dynamic_fixtures/java/spring_controller/CommandRunner.java
index 8f490e25..21268670 100644
--- a/tests/dynamic_fixtures/java/spring_controller/CommandRunner.java
+++ b/tests/dynamic_fixtures/java/spring_controller/CommandRunner.java
@@ -1,11 +1,4 @@
-// Phase 14 fixture stub — Spring-injected helper service.
-// The fixture's controller declares `@Autowired CommandRunner runner;`
-// so the harness exercises the Phase 09 import-extraction path
-// (`@Autowired` is the marker that flags `org.springframework` as a
-// transitive dep). At runtime the harness instantiates the controller
-// via reflection's default ctor — the @Autowired field stays null
-// because there is no Spring container; the controller's handler
-// guards against null and constructs a fresh CommandRunner on demand.
+// Spring-injected helper service used by the controller fixtures.
import java.io.BufferedReader;
import java.io.InputStreamReader;
diff --git a/tests/dynamic_fixtures/java/spring_controller/RequestMapping.java b/tests/dynamic_fixtures/java/spring_controller/RequestMapping.java
deleted file mode 100644
index e518a5b5..00000000
--- a/tests/dynamic_fixtures/java/spring_controller/RequestMapping.java
+++ /dev/null
@@ -1,12 +0,0 @@
-// Phase 14 fixture stub — minimal Spring `@RequestMapping`.
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.METHOD, ElementType.TYPE})
-public @interface RequestMapping {
- String value() default "";
-}
diff --git a/tests/dynamic_fixtures/java/spring_controller/RestController.java b/tests/dynamic_fixtures/java/spring_controller/RestController.java
deleted file mode 100644
index 002b93a7..00000000
--- a/tests/dynamic_fixtures/java/spring_controller/RestController.java
+++ /dev/null
@@ -1,11 +0,0 @@
-// Phase 14 fixture stub — minimal Spring `@RestController`.
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.TYPE)
-public @interface RestController {
-}
diff --git a/tests/dynamic_fixtures/java/spring_controller/Vuln.java b/tests/dynamic_fixtures/java/spring_controller/Vuln.java
index 3c96a6ec..400aca35 100644
--- a/tests/dynamic_fixtures/java/spring_controller/Vuln.java
+++ b/tests/dynamic_fixtures/java/spring_controller/Vuln.java
@@ -1,10 +1,10 @@
-// Phase 14 — Spring `@RestController`, vulnerable.
-//
-// Controller declares an `@Autowired CommandRunner` field so the
-// Phase 09 Java import-extractor sees the Spring annotation surface.
-// The harness instantiates the controller via reflection and invokes
-// `run(payload)`; the field stays null at runtime (no Spring DI), so
-// the handler constructs the helper on demand.
+// Spring `@RestController`, vulnerable.
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/run")
@@ -12,7 +12,8 @@ public class Vuln {
@Autowired
private CommandRunner runner;
- public String run(String payload) throws Exception {
+ @GetMapping
+ public String run(@RequestParam("payload") String payload) throws Exception {
System.out.print("__NYX_SINK_HIT__\n");
CommandRunner r = (runner != null) ? runner : new CommandRunner();
String out = r.run("echo hello " + payload);
diff --git a/tests/dynamic_fixtures/java/spring_controller/pom.xml b/tests/dynamic_fixtures/java/spring_controller/pom.xml
index db920a9a..8f69bfce 100644
--- a/tests/dynamic_fixtures/java/spring_controller/pom.xml
+++ b/tests/dynamic_fixtures/java/spring_controller/pom.xml
@@ -14,10 +14,26 @@
spring-web
6.1.5
+
+ org.springframework
+ spring-webmvc
+ 6.1.5
+
org.springframework
spring-context
6.1.5
+
+ org.springframework
+ spring-test
+ 6.1.5
+ test
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ 6.0.0
+
diff --git a/tests/java_fixtures.rs b/tests/java_fixtures.rs
index 3b392665..aa4580b3 100644
--- a/tests/java_fixtures.rs
+++ b/tests/java_fixtures.rs
@@ -674,7 +674,7 @@ mod phase14_shape_tests {
"Vuln.java",
"run",
Cap::CODE_EXEC,
- 16,
+ 19,
EntryKind::HttpRoute,
PayloadSlot::Param(0),
) else {
@@ -690,7 +690,7 @@ mod phase14_shape_tests {
"Benign.java",
"run",
Cap::CODE_EXEC,
- 14,
+ 22,
EntryKind::HttpRoute,
PayloadSlot::Param(0),
) else {