From 61bfc0cf96b3760ec3f3a884f8f16ce89df36633 Mon Sep 17 00:00:00 2001 From: elipeter Date: Tue, 26 May 2026 09:57:31 -0500 Subject: [PATCH] refactor(dynamic): replace Spring annotation stubs with real dependencies, integrate MockMvc-based invocation for Spring controllers, and enhance runtime classpath logic --- .gitignore | 1 + src/dynamic/harness.rs | 16 ++- src/dynamic/lang/java.rs | 136 +++++++++++++++--- src/dynamic/lang/java_owasp_stubs.rs | 17 +-- src/dynamic/runner.rs | 10 +- src/dynamic/sandbox/mod.rs | 8 +- tests/benchmark/RESULTS.md | 3 +- tests/common/fixture_harness.rs | 28 +++- .../java/spring_controller/Autowired.java | 13 -- .../java/spring_controller/Benign.java | 11 +- .../java/spring_controller/CommandRunner.java | 9 +- .../spring_controller/RequestMapping.java | 12 -- .../spring_controller/RestController.java | 11 -- .../java/spring_controller/Vuln.java | 17 +-- .../java/spring_controller/pom.xml | 16 +++ tests/java_fixtures.rs | 4 +- 16 files changed, 214 insertions(+), 98 deletions(-) delete mode 100644 tests/dynamic_fixtures/java/spring_controller/Autowired.java delete mode 100644 tests/dynamic_fixtures/java/spring_controller/RequestMapping.java delete mode 100644 tests/dynamic_fixtures/java/spring_controller/RestController.java 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 {