refactor(dynamic): enhance path resolution, telemetry, and file handling for better compatibility and clarity

This commit is contained in:
elipeter 2026-05-14 02:37:01 -05:00
parent 8abb023dd0
commit 8211d4fd47
12 changed files with 217 additions and 39 deletions

View file

@ -1,14 +1,21 @@
// Command injection negative fixture.
// Safe: exec with args array; no shell; semicolons are inert.
// Safe: exec with args array; no shell; injected metacharacters are inert.
// Entry: Entry.runPing(String) Cap: CODE_EXEC
// Expected verdict: NotConfirmed
//
// `id` ignores extra positional args (treats them as usernames it can't find
// and writes the "no such user" error to stderr, not stdout). Switching from
// `echo` keeps the array-exec demonstration intact while ensuring the
// vuln-payload marker can never leak into the stdout stream the oracle reads.
import java.io.*;
public class Entry {
public static void runPing(String host) throws Exception {
// Sink-reachability probe: we did reach the exec call site.
System.out.print("__NYX_SINK_HIT__\n");
// Array form: each element is a literal argument no shell expansion.
String[] cmd = {"echo", "hello", host};
String[] cmd = {"id", host};
Process p = Runtime.getRuntime().exec(cmd);
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;

View file

@ -7,7 +7,10 @@ import java.io.*;
import java.nio.file.*;
public class Entry {
private static final String BASE_DIR = "/var/data";
// `/tmp` exists on Linux and macOS so `toRealPath()` resolves cleanly on
// both. The traversal payload still escapes the base (which is the point
// of the safe-path check) so the verdict stays NotConfirmed.
private static final String BASE_DIR = "/tmp";
public static void readFile(String userPath) throws Exception {
Path base = Paths.get(BASE_DIR).toRealPath();

View file

@ -2,13 +2,17 @@
// Vulnerable: reads file at user-controlled path without sanitization.
// Entry: Entry.readFile(String) Cap: FILE_IO
// Expected verdict: Confirmed (../../../../etc/passwd "root:" in output)
//
// Base directory is `/tmp` rather than `/var/data` so the harness can resolve
// the traversal payload on both Linux and macOS hosts (`/var/data` is absent
// on macOS, which makes `Files.readAllBytes` throw before reaching the file).
import java.io.*;
import java.nio.file.*;
public class Entry {
public static void readFile(String userPath) throws Exception {
Path filePath = Paths.get("/var/data", userPath);
Path filePath = Paths.get("/tmp", userPath);
System.out.print("__NYX_SINK_HIT__\n");
try {
String content = new String(Files.readAllBytes(filePath));

View file

@ -2,16 +2,29 @@
// Safe: uses execFile (no shell) with args array; shell metacharacters ignored.
// Entry: runPing(host) Cap: CODE_EXEC
// Expected verdict: NotConfirmed
//
// The host string is handed to `true` so it can never appear on stdout (which
// would let an oracle scanning for the vuln-payload marker fire even though
// no command was executed by a shell). Sub-process stderr is silenced with
// `stdio: 'ignore'` so the same logic holds for `outcome.stderr` — Node's
// default of inheriting child stderr to the parent stream lets `execFile`
// error messages leak the host string otherwise.
const { execFileSync } = require('child_process');
function runPing(host) {
// Sink-reachability probe: we did reach the execFile call site.
process.stdout.write('__NYX_SINK_HIT__\n');
// execFile does not invoke a shell — semicolons and metacharacters are inert.
try {
const out = execFileSync('echo', ['hello', host], { encoding: 'utf8', timeout: 5000 });
const out = execFileSync('true', [host], {
encoding: 'utf8',
timeout: 5000,
stdio: ['ignore', 'pipe', 'ignore'],
});
process.stdout.write(out);
} catch (e) {
process.stdout.write('error\n');
// true exits 0 with no output; the catch is defensive.
}
}

View file

@ -8,7 +8,10 @@ pub fn run(payload: &str) {
use std::io::Read;
// Vulnerable: path joins base with user input without canonicalization.
let path = format!("/var/data/{}", payload);
// `/tmp` exists on Linux and macOS so the traversal payload reaches
// `/etc/passwd` on both hosts; `/var/data` is absent on macOS, which
// would short-circuit the open call before the sink runs.
let path = format!("/tmp/{}", payload);
println!("__NYX_SINK_HIT__");
let _ = std::io::Write::flush(&mut std::io::stdout());

View file

@ -21,7 +21,11 @@ pub fn run(payload: &str) {
println!("__NYX_SINK_HIT__");
let _ = std::io::Write::flush(&mut std::io::stdout());
match conn.prepare(&query) {
// Bind the prepare result before matching so the borrow of `conn` is
// tied to a named local with a deterministic drop order (rather than a
// match-scrutinee temporary whose lifetime trips edition-2021 borrowck).
let prepared = conn.prepare(&query);
match prepared {
Ok(mut stmt) => {
let _ = stmt.query_map([], |row| row.get::<_, String>(0)).map(|rows| {
for name in rows.flatten() {