refactor(dynamic): add recursive dependency resolution for Java, Go, and Ruby receivers, expand corresponding tests

This commit is contained in:
elipeter 2026-05-24 21:45:54 -05:00
parent 0e8c900078
commit acec041676
10 changed files with 366 additions and 10 deletions

View file

@ -182,14 +182,26 @@ fn class_method_java_emits_reflective_dispatch() {
let h = lang::emit(&spec).expect("emit ok");
assert!(h.source.contains("Class.forName"));
assert!(h.source.contains("nyxBuildReceiver"));
assert!(h.source.contains("nyxValueForType(params[i], depth - 1"));
assert!(h.source.contains("Object result = match.invoke"));
assert!(h.source.contains("UserRepository"));
}
#[test]
fn class_method_ruby_dispatch_builds_recursive_receiver() {
let spec = make_spec(Lang::Ruby);
let h = lang::emit(&spec).expect("emit ok");
assert!(h.source.contains("_nyx_build_receiver(cls, depth = 3"));
assert!(h.source.contains("_nyx_const_for_param"));
assert!(h.source.contains("depth - 1"));
}
#[test]
fn class_method_go_uses_reflect_receivers_registry() {
let spec = make_spec(Lang::Go);
let h = lang::emit(&spec).expect("emit ok");
assert!(h.source.contains("entry.NyxAutoReceivers"));
assert!(h.source.contains("nyxPopulateReceiver"));
assert!(h.source.contains("MethodByName"));
let registry = h
.extra_files
@ -284,6 +296,17 @@ mod e2e_phase_19 {
cap: Cap::CODE_EXEC,
bins: &["ruby"],
},
Case {
lang: Lang::Ruby,
fixture_dir: "ruby_recursive_deps",
vuln_file: "vuln.rb",
benign_file: "benign.rb",
vuln_class: "UserService",
benign_class: "UserService",
method: "run",
cap: Cap::CODE_EXEC,
bins: &["ruby"],
},
Case {
lang: Lang::JavaScript,
fixture_dir: "javascript",
@ -361,6 +384,17 @@ mod e2e_phase_19 {
cap: Cap::CODE_EXEC,
bins: &["java", "javac"],
},
Case {
lang: Lang::Java,
fixture_dir: "java_recursive_deps",
vuln_file: "Vuln.java",
benign_file: "Benign.java",
vuln_class: "Vuln$UserService",
benign_class: "Benign$UserService",
method: "run",
cap: Cap::CODE_EXEC,
bins: &["java", "javac"],
},
Case {
lang: Lang::Go,
fixture_dir: "go",
@ -372,6 +406,17 @@ mod e2e_phase_19 {
cap: Cap::CODE_EXEC,
bins: &["go"],
},
Case {
lang: Lang::Go,
fixture_dir: "go_recursive_deps",
vuln_file: "vuln.go",
benign_file: "benign.go",
vuln_class: "UserService",
benign_class: "UserService",
method: "Run",
cap: Cap::CODE_EXEC,
bins: &["go"],
},
Case {
lang: Lang::Rust,
fixture_dir: "rust",

View file

@ -0,0 +1,32 @@
// Benign control for recursively populated Go struct dependencies.
package entry
import "strings"
type ShellRunner struct{}
func (ShellRunner) Run(command string) string {
return strings.ReplaceAll(command, "NYX_PWN_CMDI", "")
}
type UserRepository struct {
Runner *ShellRunner
}
func (r UserRepository) Find(input string) string {
if r.Runner == nil {
return ""
}
return r.Runner.Run(input)
}
type UserService struct {
Repository *UserRepository
}
func (s UserService) Run(input string) string {
if s.Repository == nil {
return ""
}
return s.Repository.Find(input)
}

View file

@ -0,0 +1,33 @@
// Class-method fixture with recursively populated Go struct dependencies.
package entry
import "os/exec"
type ShellRunner struct{}
func (ShellRunner) Run(command string) string {
out, _ := exec.Command("sh", "-c", "true "+command).Output()
return string(out)
}
type UserRepository struct {
Runner *ShellRunner
}
func (r UserRepository) Find(input string) string {
if r.Runner == nil {
return ""
}
return r.Runner.Run(input)
}
type UserService struct {
Repository *UserRepository
}
func (s UserService) Run(input string) string {
if s.Repository == nil {
return ""
}
return s.Repository.Find(input)
}

View file

@ -0,0 +1,32 @@
// Benign control for recursively constructed Java dependencies.
public class Benign {
public static class ShellRunner {
public String run(String command) {
return command.replace("NYX_PWN_CMDI", "");
}
}
public static class UserRepository {
private final ShellRunner shellRunner;
public UserRepository(ShellRunner shellRunner) {
this.shellRunner = shellRunner;
}
public String find(String input) {
return shellRunner.run(input);
}
}
public static class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public String run(String input) {
return userRepository.find(input);
}
}
}

View file

@ -0,0 +1,39 @@
// Class-method fixture with recursively constructed Java dependencies.
import java.io.InputStream;
public class Vuln {
public static class ShellRunner {
public String run(String command) throws Exception {
Process p = new ProcessBuilder("sh", "-c", "true " + command)
.redirectErrorStream(true)
.start();
try (InputStream in = p.getInputStream()) {
return new String(in.readAllBytes());
}
}
}
public static class UserRepository {
private final ShellRunner shellRunner;
public UserRepository(ShellRunner shellRunner) {
this.shellRunner = shellRunner;
}
public String find(String input) throws Exception {
return shellRunner.run(input);
}
}
public static class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public String run(String input) throws Exception {
return userRepository.find(input);
}
}
}

View file

@ -0,0 +1,26 @@
# Benign control for recursively constructed Ruby dependencies.
class ShellRunner
def run(command)
command.gsub('NYX_PWN_CMDI', '')
end
end
class UserRepository
def initialize(shell_runner)
@shell_runner = shell_runner
end
def find(input)
@shell_runner.run(input)
end
end
class UserService
def initialize(user_repository)
@user_repository = user_repository
end
def run(input)
@user_repository.find(input)
end
end

View file

@ -0,0 +1,26 @@
# Class-method fixture with recursively constructed Ruby dependencies.
class ShellRunner
def run(command)
`true #{command}`
end
end
class UserRepository
def initialize(shell_runner)
@shell_runner = shell_runner
end
def find(input)
@shell_runner.run(input)
end
end
class UserService
def initialize(user_repository)
@user_repository = user_repository
end
def run(input)
@user_repository.find(input)
end
end