[pitboss] phase 04: M4 — Rust harness (second-language validation)

This commit is contained in:
pitboss 2026-05-12 00:57:45 -04:00
parent e875aa1208
commit 3ffe480660
37 changed files with 1872 additions and 54 deletions

View file

@ -0,0 +1,11 @@
[package]
name = "nyx-escape-test"
version = "0.1.0"
edition = "2021"
build = "build.rs"
[[bin]]
name = "nyx_escape_harness"
path = "src/main.rs"
[dependencies]

View file

@ -0,0 +1,16 @@
// Escape attempt: malicious build.rs that tries to write a host marker file
// during `cargo build`. A sandboxed build should prevent this.
//
// Expected behaviour in an isolated sandbox: file NOT created, build fails
// or marker stays absent.
// Expected behaviour on unsandboxed host: marker would appear at /tmp/pwned_build_rs.
fn main() {
// Attempt to write a marker file on the host.
let marker = std::path::Path::new("/tmp/pwned_build_rs");
if std::fs::File::create(marker).is_ok() {
// Escape succeeded: signal via stdout (cargo captures build script stdout).
println!("cargo:warning=NYX_ESCAPE_SUCCESS: build.rs created /tmp/pwned_build_rs");
} else {
println!("cargo:warning=BLOCKED: could not write /tmp/pwned_build_rs");
}
}

View file

@ -0,0 +1,4 @@
// Benign entry point — the escape attempt lives in build.rs, not here.
fn main() {
println!("nyx_escape_harness: no vulnerability in main");
}

View file

@ -0,0 +1,13 @@
/// Command injection — adversarial collision fixture.
///
/// Prints "NYX_PWN_CMDI" unconditionally without going through a shell sink.
///
/// Expected verdict: Inconclusive(OracleCollisionSuspected)
/// oracle_fired = true (OutputContains("NYX_PWN_CMDI"))
/// sink_hit = false (__NYX_SINK_HIT__ never printed)
/// Cap: CODE_EXEC Entry: `run(payload: &str)`
pub fn run(payload: &str) {
// Coincidental oracle match — not a command execution sink.
println!("NYX_PWN_CMDI");
let _ = payload.len();
}

View file

@ -0,0 +1,23 @@
/// Command injection — negative fixture.
///
/// Safe function: uses Command with a list of args (no shell expansion).
/// Payload is used as a literal argument, not interpreted by the shell.
/// Expected verdict: NotConfirmed.
/// Cap: CODE_EXEC Entry: `run(payload: &str)`
pub fn run(payload: &str) {
use std::process::Command;
// Safe: list-form args — shell metacharacters in payload are inert.
let safe_target = payload
.chars()
.filter(|c| c.is_alphanumeric() || *c == '.')
.collect::<String>();
println!("__NYX_SINK_HIT__");
let _ = std::io::Write::flush(&mut std::io::stdout());
match Command::new("echo").arg(&safe_target).output() {
Ok(out) => print!("{}", String::from_utf8_lossy(&out.stdout)),
Err(e) => eprintln!("exec error: {}", e),
}
}

View file

@ -0,0 +1,24 @@
/// Command injection — positive fixture.
///
/// Vulnerable function: builds a shell command string from user input.
/// Expected verdict: Confirmed (payload "; echo NYX_PWN_CMDI" echoes the marker).
/// Cap: CODE_EXEC Entry: `run(payload: &str)`
pub fn run(payload: &str) {
use std::process::Command;
// Vulnerable: user input concatenated into shell command string.
let cmd = format!("echo {}", payload);
println!("__NYX_SINK_HIT__");
let _ = std::io::Write::flush(&mut std::io::stdout());
match Command::new("sh").args(["-c", &cmd]).output() {
Ok(out) => {
print!("{}", String::from_utf8_lossy(&out.stdout));
if !out.stderr.is_empty() {
eprint!("{}", String::from_utf8_lossy(&out.stderr));
}
}
Err(e) => eprintln!("exec error: {}", e),
}
}

View file

@ -0,0 +1,25 @@
/// Command injection — second positive fixture.
///
/// Variant: builds a script filename from user input and passes it to sh.
/// Expected verdict: Confirmed (payload "; echo NYX_PWN_CMDI" injects into the
/// command string at a different AST site than cmdi_positive.rs).
/// Cap: CODE_EXEC Entry: `run(payload: &str)`
pub fn run(payload: &str) {
use std::process::Command;
// Vulnerable: payload used as a path argument, which is shell-interpolated.
let script = format!("ls -la {}", payload);
println!("__NYX_SINK_HIT__");
let _ = std::io::Write::flush(&mut std::io::stdout());
match Command::new("sh").args(["-c", &script]).output() {
Ok(out) => {
print!("{}", String::from_utf8_lossy(&out.stdout));
if !out.stderr.is_empty() {
eprint!("{}", String::from_utf8_lossy(&out.stderr));
}
}
Err(e) => eprintln!("exec error: {}", e),
}
}

View file

@ -0,0 +1,21 @@
/// Command injection — unsupported entry-kind fixture.
///
/// Vulnerable logic lives inside a struct method. The test creates a Diag
/// with an unsupported entry kind so `HarnessSpec::from_finding` returns
/// `Err(UnsupportedReason::EntryKindUnsupported)`.
///
/// Expected verdict: Unsupported(EntryKindUnsupported)
/// Cap: CODE_EXEC
pub struct ShellRunner;
impl ShellRunner {
pub fn execute(&self, user_cmd: &str) -> Option<String> {
use std::process::Command;
let cmd = format!("run {}", user_cmd);
Command::new("sh")
.args(["-c", &cmd])
.output()
.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
}
}

View file

@ -0,0 +1,14 @@
/// File I/O — adversarial collision fixture.
///
/// Prints "root:" unconditionally without opening any file or printing the
/// sink-reachability sentinel.
///
/// Expected verdict: Inconclusive(OracleCollisionSuspected)
/// oracle_fired = true (OutputContains("root:"))
/// sink_hit = false (__NYX_SINK_HIT__ never printed)
/// Cap: FILE_IO Entry: `run(payload: &str)`
pub fn run(payload: &str) {
// Coincidental oracle match — no file I/O sink involved.
println!("root:x:0:0:root:/root:/bin/bash");
let _ = payload.len();
}

View file

@ -0,0 +1,27 @@
/// File I/O — negative fixture.
///
/// Safe function: reads from a fixed path; user input is only used as a search
/// term within file contents, not as the file path itself.
/// Expected verdict: NotConfirmed.
/// Cap: FILE_IO Entry: `run(payload: &str)`
pub fn run(payload: &str) {
// Safe: path is hard-coded; payload cannot influence which file is read.
let fixed_path = "/tmp/nyx_safe_file_does_not_exist";
println!("__NYX_SINK_HIT__");
let _ = std::io::Write::flush(&mut std::io::stdout());
match std::fs::read_to_string(fixed_path) {
Ok(contents) => {
// Only use payload as a filter, not as a path.
for line in contents.lines() {
if line.contains(payload) {
println!("{}", line);
}
}
}
Err(_) => {
println!("file not found (expected in test)");
}
}
}

View file

@ -0,0 +1,16 @@
/// File I/O — positive fixture.
///
/// Vulnerable function: reads a file at a user-controlled path.
/// Expected verdict: Confirmed (path-traversal payload "../../../../etc/passwd"
/// causes "root:" to appear in stdout).
/// Cap: FILE_IO Entry: `run(payload: &str)`
pub fn run(payload: &str) {
println!("__NYX_SINK_HIT__");
let _ = std::io::Write::flush(&mut std::io::stdout());
// Vulnerable: user controls the file path — path traversal possible.
match std::fs::read_to_string(payload) {
Ok(contents) => print!("{}", contents),
Err(e) => eprintln!("Error reading {}: {}", payload, e),
}
}

View file

@ -0,0 +1,24 @@
/// File I/O — second positive fixture.
///
/// Variant: uses std::fs::File::open instead of read_to_string; path constructed
/// from a base directory and user-supplied component (still traversable).
/// Expected verdict: Confirmed (payload "../../../../etc/passwd" reaches /etc/passwd).
/// Cap: FILE_IO Entry: `run(payload: &str)`
pub fn run(payload: &str) {
use std::io::Read;
// Vulnerable: path joins base with user input without canonicalization.
let path = format!("/var/data/{}", payload);
println!("__NYX_SINK_HIT__");
let _ = std::io::Write::flush(&mut std::io::stdout());
match std::fs::File::open(&path) {
Ok(mut f) => {
let mut buf = String::new();
let _ = f.read_to_string(&mut buf);
print!("{}", buf);
}
Err(e) => eprintln!("Error opening {}: {}", path, e),
}
}

View file

@ -0,0 +1,16 @@
/// File I/O — unsupported entry-kind fixture.
///
/// Vulnerable logic lives inside a struct method. The test creates a Diag
/// with an unsupported entry kind so `HarnessSpec::from_finding` returns
/// `Err(UnsupportedReason::EntryKindUnsupported)`.
///
/// Expected verdict: Unsupported(EntryKindUnsupported)
/// Cap: FILE_IO
pub struct FileService;
impl FileService {
pub fn read(&self, path: &str) -> String {
// Vulnerable: path traversal — user controls the path.
std::fs::read_to_string(path).unwrap_or_default()
}
}

View file

@ -0,0 +1,15 @@
/// SQL injection — adversarial collision fixture.
///
/// Prints "NYX_SQL_CONFIRMED" unconditionally without going through a SQL sink
/// and without printing the sink-reachability sentinel.
///
/// Expected verdict: Inconclusive(OracleCollisionSuspected)
/// oracle_fired = true (OutputContains("NYX_SQL_CONFIRMED"))
/// sink_hit = false (__NYX_SINK_HIT__ never printed)
/// Cap: SQL_QUERY Entry: `run(payload: &str)`
pub fn run(payload: &str) {
// Coincidental oracle match — not a SQL sink.
println!("NYX_SQL_CONFIRMED");
// Ensure payload is consumed so the compiler does not optimise it away.
let _ = payload.len();
}

View file

@ -0,0 +1,33 @@
/// SQL injection — negative fixture.
///
/// Safe function: uses parameterized query (rusqlite params![]).
/// Expected verdict: NotConfirmed (no injection possible; oracle cannot fire).
/// Cap: SQL_QUERY Entry: `run(payload: &str)`
pub fn run(payload: &str) {
use rusqlite::Connection;
let conn = Connection::open_in_memory().expect("open in-memory db");
conn.execute_batch(
"CREATE TABLE users (id INTEGER, name TEXT);\
INSERT INTO users VALUES (1, 'alice');\
INSERT INTO users VALUES (2, 'bob');",
)
.expect("setup schema");
// Safe: parameterized query — payload cannot escape the literal binding.
let mut stmt = conn
.prepare("SELECT name FROM users WHERE name=?1")
.expect("prepare");
// Sink reached via safe parameterized path; sentinel fires but oracle will not.
println!("__NYX_SINK_HIT__");
let _ = std::io::Write::flush(&mut std::io::stdout());
let _ = stmt
.query_map(rusqlite::params![payload], |row| row.get::<_, String>(0))
.map(|rows| {
for name in rows.flatten() {
println!("{}", name);
}
});
}

View file

@ -0,0 +1,38 @@
/// SQL injection — positive fixture.
///
/// Vulnerable function: directly concatenates user input into SQL.
/// Expected verdict: Confirmed (UNION payload causes "NYX_SQL_CONFIRMED" in output).
/// Cap: SQL_QUERY Entry: `run(payload: &str)`
pub fn run(payload: &str) {
use rusqlite::Connection;
let conn = Connection::open_in_memory().expect("open in-memory db");
conn.execute_batch(
"CREATE TABLE users (id INTEGER, name TEXT);\
INSERT INTO users VALUES (1, 'alice');\
INSERT INTO users VALUES (2, 'bob');",
)
.expect("setup schema");
// Vulnerable: direct string concatenation into SQL.
let query = format!("SELECT name FROM users WHERE name='{}'", payload);
// Sentinel: the sink (conn.prepare) is reachable with tainted input.
println!("__NYX_SINK_HIT__");
let _ = std::io::Write::flush(&mut std::io::stdout());
match conn.prepare(&query) {
Ok(mut stmt) => {
let _ = stmt.query_map([], |row| row.get::<_, String>(0)).map(|rows| {
for name in rows.flatten() {
println!("{}", name);
}
});
}
Err(e) => {
// Error-based: print query on failure (oracle can detect via query echo).
println!("DB query: {}", query);
println!("DB error: {}", e);
}
}
}

View file

@ -0,0 +1,24 @@
/// SQL injection — unsupported entry-kind fixture.
///
/// The vulnerable logic lives inside a struct method. The test creates a Diag
/// with an unsupported entry kind, so `HarnessSpec::from_finding` returns
/// `Err(UnsupportedReason::EntryKindUnsupported)`.
///
/// Expected verdict: Unsupported(EntryKindUnsupported)
/// Cap: SQL_QUERY
pub struct UserRepository;
impl UserRepository {
pub fn find_user(&self, name: &str) -> Vec<String> {
use rusqlite::Connection;
let conn = Connection::open_in_memory().expect("open db");
let query = format!("SELECT name FROM users WHERE name='{}'", name);
match conn.prepare(&query) {
Ok(mut stmt) => stmt
.query_map([], |row| row.get::<_, String>(0))
.map(|rows| rows.flatten().collect())
.unwrap_or_default(),
Err(_) => vec![],
}
}
}

View file

@ -0,0 +1,38 @@
/// SQL injection fixture — same vulnerability as sqli_positive, placed in a
/// directory that contains a secrets file (.env with AWS key).
///
/// The test verifies that the AWS key is redacted from outcome.json / telemetry
/// and never appears in any repro artifact after verification.
///
/// Expected verdict: Confirmed (same oracle as sqli_positive)
/// Cap: SQL_QUERY Entry: `run(payload: &str)`
pub fn run(payload: &str) {
use rusqlite::Connection;
let conn = Connection::open_in_memory().expect("open in-memory db");
conn.execute_batch(
"CREATE TABLE users (id INTEGER, name TEXT);\
INSERT INTO users VALUES (1, 'alice');\
INSERT INTO users VALUES (2, 'bob');",
)
.expect("setup schema");
let query = format!("SELECT name FROM users WHERE name='{}'", payload);
println!("__NYX_SINK_HIT__");
let _ = std::io::Write::flush(&mut std::io::stdout());
match conn.prepare(&query) {
Ok(mut stmt) => {
let _ = stmt.query_map([], |row| row.get::<_, String>(0)).map(|rows| {
for name in rows.flatten() {
println!("{}", name);
}
});
}
Err(e) => {
println!("DB query: {}", query);
println!("DB error: {}", e);
}
}
}

View file

@ -0,0 +1,14 @@
/// SSRF — adversarial collision fixture.
///
/// Prints "daemon:" unconditionally without making any network or file request,
/// and without printing the sink-reachability sentinel.
///
/// Expected verdict: Inconclusive(OracleCollisionSuspected)
/// oracle_fired = true (OutputContains("daemon:"))
/// sink_hit = false (__NYX_SINK_HIT__ never printed)
/// Cap: SSRF Entry: `run(payload: &str)`
pub fn run(payload: &str) {
// Coincidental oracle match — no URL fetch or network sink involved.
println!("daemon:*:1:1:System Services:/var/root:/usr/bin/false");
let _ = payload.len();
}

View file

@ -0,0 +1,20 @@
/// SSRF — negative fixture.
///
/// Safe function: URL is fixed; user input is used only as a query parameter,
/// not as the URL origin.
/// Expected verdict: NotConfirmed.
/// Cap: SSRF Entry: `run(payload: &str)`
pub fn run(payload: &str) {
// Safe: payload is a query value, not the URL itself — origin is fixed.
let url = format!("file:///tmp/safe_data?q={}", payload);
println!("__NYX_SINK_HIT__");
let _ = std::io::Write::flush(&mut std::io::stdout());
// Extract the fixed path (no user control over scheme or host).
let path = "/tmp/safe_data";
match std::fs::read_to_string(path) {
Ok(content) => print!("{}", content),
Err(_) => println!("resource not available (expected in test): {}", url),
}
}

View file

@ -0,0 +1,26 @@
/// SSRF — positive fixture.
///
/// Vulnerable function: fetches a user-controlled URL. Implements a minimal
/// file:// scheme reader so the test requires no network and no async runtime.
///
/// Expected verdict: Confirmed (payload "file:///etc/passwd" causes "daemon:"
/// to appear in stdout via the file:// scheme handler).
/// Cap: SSRF Entry: `run(payload: &str)`
pub fn run(payload: &str) {
println!("__NYX_SINK_HIT__");
let _ = std::io::Write::flush(&mut std::io::stdout());
// Vulnerable: user controls the URL — SSRF via file:// scheme reaches local files.
let result = fetch_url(payload);
print!("{}", result);
}
fn fetch_url(url: &str) -> String {
if let Some(path) = url.strip_prefix("file://") {
std::fs::read_to_string(path)
.unwrap_or_else(|e| format!("fetch error: {}", e))
} else {
// For non-file schemes, report the target (demonstrating SSRF intent).
format!("SSRF: would connect to {}", url)
}
}

View file

@ -0,0 +1,32 @@
/// SSRF — second positive fixture.
///
/// Variant: user-controlled URL stored in a struct field before being fetched,
/// exercising a different taint path than ssrf_positive.rs.
/// Expected verdict: Confirmed (payload "file:///etc/passwd" reaches the file
/// reader via the stored URL field).
/// Cap: SSRF Entry: `run(payload: &str)`
pub fn run(payload: &str) {
let req = Request { url: payload.to_owned() };
println!("__NYX_SINK_HIT__");
let _ = std::io::Write::flush(&mut std::io::stdout());
let result = req.execute();
print!("{}", result);
}
struct Request {
url: String,
}
impl Request {
fn execute(&self) -> String {
// Vulnerable: self.url derived from user input — SSRF.
if let Some(path) = self.url.strip_prefix("file://") {
std::fs::read_to_string(path)
.unwrap_or_else(|e| format!("fetch error: {}", e))
} else {
format!("SSRF: would connect to {}", self.url)
}
}
}

View file

@ -0,0 +1,20 @@
/// SSRF — unsupported entry-kind fixture.
///
/// Vulnerable logic lives inside a struct method. The test creates a Diag
/// with an unsupported entry kind so `HarnessSpec::from_finding` returns
/// `Err(UnsupportedReason::EntryKindUnsupported)`.
///
/// Expected verdict: Unsupported(EntryKindUnsupported)
/// Cap: SSRF
pub struct HttpClient;
impl HttpClient {
pub fn get(&self, url: &str) -> String {
// Vulnerable: user controls the URL — SSRF.
if let Some(path) = url.strip_prefix("file://") {
std::fs::read_to_string(path).unwrap_or_default()
} else {
format!("fetching: {}", url)
}
}
}