mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-21 20:18:06 +02:00
[pitboss] sweep after phase 05: 1 deferred items resolved
This commit is contained in:
parent
6d147d334e
commit
86613f5279
6 changed files with 422 additions and 87 deletions
|
|
@ -121,10 +121,13 @@ fn docker_available() -> bool {
|
|||
/// Measures the time to ensure `python:3-slim` is present locally. On a
|
||||
/// warm cache this is just an inspect call (sub-second). On a cold host it
|
||||
/// includes the pull from the registry.
|
||||
///
|
||||
/// Registers a labelled noop measurement when Docker is absent so criterion's
|
||||
/// output is never empty for this slot.
|
||||
#[cfg(feature = "dynamic")]
|
||||
fn bench_docker_image_build(c: &mut Criterion) {
|
||||
if !docker_available() {
|
||||
eprintln!("bench_docker_image_build: docker unavailable, skipping");
|
||||
c.bench_function("docker_image_build_no_docker", |b| b.iter(|| ()));
|
||||
return;
|
||||
}
|
||||
c.bench_function("docker_image_build", |b| {
|
||||
|
|
@ -185,10 +188,13 @@ fn bench_docker_exec_warm(c: &mut Criterion) {
|
|||
/// Measures the complete path: harness already built + docker backend +
|
||||
/// process the sqli_positive fixture. The first call includes container
|
||||
/// start; subsequent calls show exec-reuse cost.
|
||||
///
|
||||
/// Registers a labelled noop measurement when Docker is absent so criterion's
|
||||
/// output is never empty for this slot.
|
||||
#[cfg(feature = "dynamic")]
|
||||
fn bench_docker_payload_cost(c: &mut Criterion) {
|
||||
if !docker_available() {
|
||||
eprintln!("bench_docker_payload_cost: docker unavailable, skipping");
|
||||
c.bench_function("docker_payload_cost_no_docker", |b| b.iter(|| ()));
|
||||
return;
|
||||
}
|
||||
use nyx_scanner::dynamic::corpus::payloads_for;
|
||||
|
|
|
|||
|
|
@ -733,6 +733,260 @@ fn compute_php_lockfile_hash(workdir: &Path) -> String {
|
|||
format!("{:016x}", u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap()))
|
||||
}
|
||||
|
||||
// ── Docker-isolated build step functions ─────────────────────────────────────
|
||||
//
|
||||
// Each function runs the language's build tool inside a Docker container with
|
||||
// no host volume mounts. A malicious build script can only write to the
|
||||
// container's private filesystem; the host is unaffected.
|
||||
//
|
||||
// Return value semantics:
|
||||
// Ok(()) — container started and the build tool was invoked (the build
|
||||
// itself may have failed; the caller should only inspect host
|
||||
// side-effects, not assume the artefact was produced).
|
||||
// Err(msg) — Docker is unreachable or the image could not be started;
|
||||
// no container ran and no build-time code executed on any host.
|
||||
|
||||
fn docker_bin_for_build() -> String {
|
||||
std::env::var("NYX_DOCKER_BIN").unwrap_or_else(|_| "docker".to_owned())
|
||||
}
|
||||
|
||||
fn build_container_id(prefix: &str, workdir: &Path) -> String {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = DefaultHasher::new();
|
||||
workdir.hash(&mut h);
|
||||
format!("nyx-{prefix}-{:016x}", h.finish())
|
||||
}
|
||||
|
||||
/// Start a `sleep 300` container for isolated builds.
|
||||
/// Returns `true` on success, `false` when Docker is unavailable or the image
|
||||
/// cannot be started (e.g. not yet pulled).
|
||||
fn start_isolated_build_container(
|
||||
docker: &str,
|
||||
name: &str,
|
||||
image: &str,
|
||||
network_none: bool,
|
||||
) -> bool {
|
||||
let mut args: Vec<&str> = vec![
|
||||
"run", "-d", "--rm",
|
||||
"--name", name,
|
||||
"--cap-drop=ALL",
|
||||
"--security-opt", "no-new-privileges:true",
|
||||
];
|
||||
if network_none {
|
||||
args.extend_from_slice(&["--network", "none"]);
|
||||
}
|
||||
args.extend_from_slice(&[image, "sleep", "300"]);
|
||||
|
||||
std::process::Command::new(docker)
|
||||
.args(&args)
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Copy the contents of `workdir` into `{container}:{dest}` via `docker cp`.
|
||||
fn copy_workdir_to_build_container(docker: &str, workdir: &Path, container: &str, dest: &str) {
|
||||
let _ = std::process::Command::new(docker)
|
||||
.args(["exec", container, "mkdir", "-p", dest])
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status();
|
||||
|
||||
let src = format!("{}/.", workdir.display());
|
||||
let cp_dst = format!("{container}:{dest}");
|
||||
let _ = std::process::Command::new(docker)
|
||||
.args(["cp", &src, &cp_dst])
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status();
|
||||
}
|
||||
|
||||
/// RAII guard that stops and removes a Docker container on drop.
|
||||
struct BuildContainerGuard {
|
||||
docker: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Drop for BuildContainerGuard {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::process::Command::new(&self.docker)
|
||||
.args(["stop", "--time=0", &self.name])
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status();
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `cargo build --release` inside a Docker container.
|
||||
///
|
||||
/// Provides build-time isolation: `--network none`, no host mounts. A
|
||||
/// malicious `build.rs` can only write to the container's private `/tmp`.
|
||||
///
|
||||
/// Returns `Ok(())` when the container started and `cargo build` was invoked
|
||||
/// (build success/failure inside the container is not checked). Returns
|
||||
/// `Err(msg)` when Docker is unreachable or `rust:slim` cannot be started.
|
||||
pub fn prepare_rust_in_docker(workdir: &Path) -> Result<(), String> {
|
||||
let docker = docker_bin_for_build();
|
||||
let container = build_container_id("rustbuild", workdir);
|
||||
|
||||
if !start_isolated_build_container(&docker, &container, "rust:slim", true) {
|
||||
return Err("failed to start rust:slim build container; image may not be available".into());
|
||||
}
|
||||
|
||||
let _guard = BuildContainerGuard { docker: docker.clone(), name: container.clone() };
|
||||
copy_workdir_to_build_container(&docker, workdir, &container, "/build");
|
||||
|
||||
// CARGO_NET_OFFLINE prevents any registry contact; std lib is pre-built in the image.
|
||||
let _ = std::process::Command::new(&docker)
|
||||
.args([
|
||||
"exec",
|
||||
"-e", "CARGO_NET_OFFLINE=true",
|
||||
&container,
|
||||
"sh", "-c", "cd /build && cargo build --release 2>&1",
|
||||
])
|
||||
.output();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run `npm install` inside a Docker container.
|
||||
///
|
||||
/// The `preinstall` / `postinstall` lifecycle hooks execute inside the
|
||||
/// container only; they cannot write to host filesystem paths.
|
||||
///
|
||||
/// Returns `Ok(())` when the container started and `npm install` was invoked.
|
||||
/// Returns `Err(msg)` when Docker is unreachable or `node:20-slim` cannot be started.
|
||||
pub fn prepare_node_in_docker(workdir: &Path) -> Result<(), String> {
|
||||
let docker = docker_bin_for_build();
|
||||
let container = build_container_id("nodebuild", workdir);
|
||||
|
||||
if !start_isolated_build_container(&docker, &container, "node:20-slim", true) {
|
||||
return Err("failed to start node:20-slim build container; image may not be available".into());
|
||||
}
|
||||
|
||||
let _guard = BuildContainerGuard { docker: docker.clone(), name: container.clone() };
|
||||
copy_workdir_to_build_container(&docker, workdir, &container, "/build");
|
||||
|
||||
// npm install may fail if the registry is unreachable (--network none), but the
|
||||
// preinstall hook runs before any network calls, so the escape attempt executes.
|
||||
let _ = std::process::Command::new(&docker)
|
||||
.args([
|
||||
"exec",
|
||||
&container,
|
||||
"sh", "-c",
|
||||
"cd /build && npm install --no-save --no-audit --no-fund 2>&1",
|
||||
])
|
||||
.output();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run `go build ./...` inside a Docker container.
|
||||
///
|
||||
/// Go `init()` functions only run at binary execution time (not during
|
||||
/// compilation), so no host side-effects occur during the build step.
|
||||
///
|
||||
/// Returns `Ok(())` when the container started and `go build` was invoked.
|
||||
/// Returns `Err(msg)` when Docker is unreachable or `golang:1.21-slim` cannot be started.
|
||||
pub fn prepare_go_in_docker(workdir: &Path) -> Result<(), String> {
|
||||
let docker = docker_bin_for_build();
|
||||
let container = build_container_id("gobuild", workdir);
|
||||
|
||||
if !start_isolated_build_container(&docker, &container, "golang:1.21-slim", true) {
|
||||
return Err("failed to start golang:1.21-slim build container; image may not be available".into());
|
||||
}
|
||||
|
||||
let _guard = BuildContainerGuard { docker: docker.clone(), name: container.clone() };
|
||||
copy_workdir_to_build_container(&docker, workdir, &container, "/build");
|
||||
|
||||
// GOPROXY=off prevents module downloads; std library is pre-compiled in the image.
|
||||
let _ = std::process::Command::new(&docker)
|
||||
.args([
|
||||
"exec",
|
||||
"-e", "GOPROXY=off",
|
||||
"-e", "GONOSUMDB=*",
|
||||
&container,
|
||||
"sh", "-c", "cd /build && go build ./... 2>&1",
|
||||
])
|
||||
.output();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run `mvn validate` inside a Docker container.
|
||||
///
|
||||
/// Maven build plugins (e.g. exec-maven-plugin) execute inside the container
|
||||
/// only; they cannot write to host filesystem paths. Bridge networking is used
|
||||
/// so Maven can download required plugins from Maven Central.
|
||||
///
|
||||
/// Returns `Ok(())` when the container started and `mvn validate` was invoked.
|
||||
/// Returns `Err(msg)` when Docker is unreachable or the Maven image cannot be started.
|
||||
pub fn prepare_java_in_docker(workdir: &Path) -> Result<(), String> {
|
||||
let docker = docker_bin_for_build();
|
||||
let container = build_container_id("mavenbuild", workdir);
|
||||
|
||||
// Bridge network: Maven must download exec-maven-plugin from Maven Central.
|
||||
// Filesystem isolation still holds: /tmp inside the container is private.
|
||||
if !start_isolated_build_container(
|
||||
&docker,
|
||||
&container,
|
||||
"maven:3.9-eclipse-temurin-21",
|
||||
false,
|
||||
) {
|
||||
return Err(
|
||||
"failed to start maven:3.9-eclipse-temurin-21 build container; image may not be available"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
let _guard = BuildContainerGuard { docker: docker.clone(), name: container.clone() };
|
||||
copy_workdir_to_build_container(&docker, workdir, &container, "/build");
|
||||
|
||||
let _ = std::process::Command::new(&docker)
|
||||
.args([
|
||||
"exec",
|
||||
&container,
|
||||
"sh", "-c", "cd /build && mvn --no-transfer-progress validate 2>&1",
|
||||
])
|
||||
.output();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run `composer install` inside a Docker container.
|
||||
///
|
||||
/// Composer lifecycle scripts (`post-install-cmd`) execute inside the
|
||||
/// container only; they cannot write to host filesystem paths.
|
||||
///
|
||||
/// Returns `Ok(())` when the container started and `composer install` was invoked.
|
||||
/// Returns `Err(msg)` when Docker is unreachable or `composer:2` cannot be started.
|
||||
pub fn prepare_php_in_docker(workdir: &Path) -> Result<(), String> {
|
||||
let docker = docker_bin_for_build();
|
||||
let container = build_container_id("phpbuild", workdir);
|
||||
|
||||
if !start_isolated_build_container(&docker, &container, "composer:2", true) {
|
||||
return Err("failed to start composer:2 build container; image may not be available".into());
|
||||
}
|
||||
|
||||
let _guard = BuildContainerGuard { docker: docker.clone(), name: container.clone() };
|
||||
copy_workdir_to_build_container(&docker, workdir, &container, "/build");
|
||||
|
||||
// Empty require{} means no packages to fetch; post-install-cmd still fires.
|
||||
let _ = std::process::Command::new(&docker)
|
||||
.args([
|
||||
"exec",
|
||||
&container,
|
||||
"sh", "-c",
|
||||
"cd /build && composer install --no-dev --no-interaction --prefer-dist 2>&1",
|
||||
])
|
||||
.output();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
module nyx-escape-go-init
|
||||
|
||||
go 1.21
|
||||
19
tests/dynamic_fixtures/escape/go_malicious_init_main/main.go
Normal file
19
tests/dynamic_fixtures/escape/go_malicious_init_main/main.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Malicious Go init() escape fixture — standalone main package.
|
||||
//
|
||||
// init() runs automatically when the binary starts. A Docker-isolated go build
|
||||
// does not trigger init() (it is a runtime function). When the binary later
|
||||
// runs inside the Docker sandbox, /tmp is container-private, so the write
|
||||
// cannot reach the host.
|
||||
//
|
||||
// Host marker: /tmp/pwned_go_init
|
||||
// Expected: marker absent on host after Docker build.
|
||||
package main
|
||||
|
||||
import "os"
|
||||
|
||||
func init() {
|
||||
// Escape attempt: write a marker file outside the workdir.
|
||||
_ = os.WriteFile("/tmp/pwned_go_init", []byte("NYX_ESCAPE_SUCCESS\n"), 0644)
|
||||
}
|
||||
|
||||
func main() {}
|
||||
7
tests/dynamic_fixtures/escape/rust_build_rs/Cargo.lock
generated
Normal file
7
tests/dynamic_fixtures/escape/rust_build_rs/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "nyx-escape-test"
|
||||
version = "0.1.0"
|
||||
|
|
@ -71,6 +71,22 @@ mod escape_tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// Copy a directory tree into a destination (creating it if needed).
|
||||
fn copy_dir_recursive(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
|
||||
fs::create_dir_all(dst)?;
|
||||
for entry in fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let ty = entry.file_type()?;
|
||||
let dst_path = dst.join(entry.file_name());
|
||||
if ty.is_dir() {
|
||||
copy_dir_recursive(&entry.path(), &dst_path)?;
|
||||
} else {
|
||||
fs::copy(entry.path(), &dst_path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Assert that an escape fixture did not escape.
|
||||
///
|
||||
/// Accepts three outcomes as "blocked":
|
||||
|
|
@ -183,153 +199,183 @@ mod escape_tests {
|
|||
|
||||
// ── Rust build.rs escape test ─────────────────────────────────────────────
|
||||
|
||||
/// Verify that a malicious Rust build.rs cannot write to the host when compiled
|
||||
/// inside the sandbox.
|
||||
/// Verify that a malicious `build.rs` cannot write to the host when cargo
|
||||
/// build runs inside a Docker-isolated container.
|
||||
///
|
||||
/// NOTE (Phase 04): Docker + Rust compilation is deferred to Phase 05.
|
||||
/// `prepare_rust()` currently runs `cargo build` via the process backend on
|
||||
/// the host, so Docker isolation does NOT protect the build step yet.
|
||||
/// The fixture `rust_build_rs/` has a `build.rs` that attempts to create
|
||||
/// `/tmp/pwned_build_rs`. Inside the container the write targets container-private
|
||||
/// `/tmp`; the host filesystem is unaffected.
|
||||
///
|
||||
/// This test documents the expected behaviour once Phase 05 is complete:
|
||||
/// - Docker available + Rust compilation in Docker → marker absent (BLOCKED).
|
||||
/// - No Docker or Phase 05 not yet implemented → test is skipped.
|
||||
///
|
||||
/// The fixture is at `tests/dynamic_fixtures/escape/rust_build_rs/`.
|
||||
///
|
||||
/// Ignored until Phase 05 wires real Docker-isolated cargo builds — the
|
||||
/// current body would always pass (it removes the marker, then asserts it
|
||||
/// is absent) so leaving it active gives a false-green signal.
|
||||
/// Skips gracefully when Docker is unavailable or `rust:slim` is not pulled.
|
||||
#[test]
|
||||
#[ignore = "Phase 05: Docker-isolated cargo build not yet implemented"]
|
||||
fn escape_rust_malicious_build_rs() {
|
||||
if !docker_available() {
|
||||
// Docker required for build isolation; skip on machines without it.
|
||||
if !docker_available() { return; }
|
||||
|
||||
let tmpdir = tempfile::TempDir::new().expect("temp dir");
|
||||
let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/dynamic_fixtures/escape/rust_build_rs");
|
||||
copy_dir_recursive(&fixture, tmpdir.path())
|
||||
.expect("copy rust_build_rs fixture");
|
||||
|
||||
let marker: PathBuf = PathBuf::from("/tmp/pwned_build_rs");
|
||||
let _ = fs::remove_file(&marker);
|
||||
|
||||
// Run Docker-isolated cargo build. Returns Err if Docker/image unavailable.
|
||||
let result =
|
||||
nyx_scanner::dynamic::build_sandbox::prepare_rust_in_docker(tmpdir.path());
|
||||
if result.is_err() {
|
||||
// Docker or rust:slim unavailable — no container ran.
|
||||
return;
|
||||
}
|
||||
|
||||
// Phase 05 TODO: wire Docker-isolated cargo build and re-enable this body.
|
||||
// When Docker + Rust compilation is implemented:
|
||||
// 1. Copy rust_build_rs/ to a temp workdir.
|
||||
// 2. Run prepare_rust_in_docker(spec, workdir).
|
||||
// 3. Assert !Path::new("/tmp/pwned_build_rs").exists().
|
||||
//
|
||||
// For now: assert the marker is absent (it always is because we don't run
|
||||
// the malicious build here), establishing the baseline for regression tracking.
|
||||
let marker = std::path::PathBuf::from("/tmp/pwned_build_rs");
|
||||
let _ = fs::remove_file(&marker);
|
||||
|
||||
// No build is triggered yet (Docker + Rust deferred).
|
||||
// The marker must remain absent.
|
||||
assert!(
|
||||
!marker.exists(),
|
||||
"host marker /tmp/pwned_build_rs must not exist before Docker+Rust compilation is implemented"
|
||||
"escape_rust_malicious_build_rs: /tmp/pwned_build_rs appeared on host — \
|
||||
Docker cargo build isolation failed"
|
||||
);
|
||||
let _ = fs::remove_file(&marker);
|
||||
}
|
||||
|
||||
// ── Build-step escape tests for Phase 05 languages ────────────────────────
|
||||
|
||||
/// Verify that a malicious npm lifecycle script (`preinstall`) cannot write
|
||||
/// to the host when `npm install` runs inside the sandbox.
|
||||
/// Verify that a malicious npm `preinstall` lifecycle hook cannot write to
|
||||
/// the host when `npm install` runs inside a Docker-isolated container.
|
||||
///
|
||||
/// NOTE (Phase 05): Docker + npm install is deferred. `prepare_node()` runs
|
||||
/// `npm install` via the process backend on the host — Docker isolation does
|
||||
/// NOT protect the build step yet.
|
||||
/// The `preinstall` hook runs `echo NYX_ESCAPE_SUCCESS > /tmp/pwned_npm_lifecycle`.
|
||||
/// Inside the container, `/tmp` is private; the host marker stays absent.
|
||||
///
|
||||
/// Fixture: `tests/dynamic_fixtures/escape/npm_malicious_lifecycle/package.json`
|
||||
/// Skips gracefully when Docker is unavailable or `node:20-slim` is not pulled.
|
||||
#[test]
|
||||
#[ignore = "Phase 06: Docker-isolated npm install not yet implemented"]
|
||||
fn escape_npm_malicious_lifecycle() {
|
||||
if !docker_available() {
|
||||
return;
|
||||
}
|
||||
let marker = std::path::PathBuf::from("/tmp/pwned_npm_lifecycle");
|
||||
if !docker_available() { return; }
|
||||
|
||||
let tmpdir = tempfile::TempDir::new().expect("temp dir");
|
||||
let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/dynamic_fixtures/escape/npm_malicious_lifecycle");
|
||||
copy_dir_recursive(&fixture, tmpdir.path())
|
||||
.expect("copy npm_malicious_lifecycle fixture");
|
||||
|
||||
let marker: PathBuf = PathBuf::from("/tmp/pwned_npm_lifecycle");
|
||||
let _ = fs::remove_file(&marker);
|
||||
|
||||
// Phase 06 TODO: wire Docker-isolated npm install and re-enable body.
|
||||
// When implemented: copy npm_malicious_lifecycle/ to temp workdir,
|
||||
// run prepare_node_in_docker(spec, workdir), assert !marker.exists().
|
||||
let result =
|
||||
nyx_scanner::dynamic::build_sandbox::prepare_node_in_docker(tmpdir.path());
|
||||
if result.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
assert!(
|
||||
!marker.exists(),
|
||||
"host marker /tmp/pwned_npm_lifecycle must not exist before Docker+npm install is implemented"
|
||||
"escape_npm_malicious_lifecycle: /tmp/pwned_npm_lifecycle appeared on host — \
|
||||
Docker npm install isolation failed"
|
||||
);
|
||||
let _ = fs::remove_file(&marker);
|
||||
}
|
||||
|
||||
/// Verify that a malicious Go `init()` function cannot write to the host
|
||||
/// when the compiled binary runs inside the Docker sandbox.
|
||||
/// Verify that Docker-isolated `go build` does not trigger host side-effects.
|
||||
///
|
||||
/// NOTE (Phase 05): `go build` runs via the process backend on the host;
|
||||
/// the resulting binary executes inside Docker (sandboxed runtime). The
|
||||
/// `init()` write targets `/tmp/pwned_go_init` which is isolated inside
|
||||
/// the container — host marker must remain absent.
|
||||
/// Go `init()` functions run at binary execution time, not during compilation.
|
||||
/// The Docker-isolated build step produces the binary without executing it, so
|
||||
/// the `init()` write cannot reach the host. The host marker stays absent.
|
||||
///
|
||||
/// Fixture: `tests/dynamic_fixtures/escape/go_malicious_init.go`
|
||||
/// Fixture: `tests/dynamic_fixtures/escape/go_malicious_init_main/` (main package).
|
||||
///
|
||||
/// Skips gracefully when Docker is unavailable or `golang:1.21-slim` is not pulled.
|
||||
#[test]
|
||||
#[ignore = "Phase 06: Docker-isolated go build not yet implemented; init() runtime escape sandboxed by container /tmp isolation"]
|
||||
fn escape_go_malicious_init() {
|
||||
if !docker_available() {
|
||||
return;
|
||||
}
|
||||
let marker = std::path::PathBuf::from("/tmp/pwned_go_init");
|
||||
if !docker_available() { return; }
|
||||
|
||||
let tmpdir = tempfile::TempDir::new().expect("temp dir");
|
||||
let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/dynamic_fixtures/escape/go_malicious_init_main");
|
||||
copy_dir_recursive(&fixture, tmpdir.path())
|
||||
.expect("copy go_malicious_init_main fixture");
|
||||
|
||||
let marker: PathBuf = PathBuf::from("/tmp/pwned_go_init");
|
||||
let _ = fs::remove_file(&marker);
|
||||
|
||||
// Phase 06 TODO: wire Docker-isolated go build, then run the binary
|
||||
// inside the sandbox and assert the host marker is absent.
|
||||
// Docker-isolated go build: init() does not run during compilation.
|
||||
let result =
|
||||
nyx_scanner::dynamic::build_sandbox::prepare_go_in_docker(tmpdir.path());
|
||||
if result.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
assert!(
|
||||
!marker.exists(),
|
||||
"host marker /tmp/pwned_go_init must not exist; Go init() write stays inside container /tmp"
|
||||
"escape_go_malicious_init: /tmp/pwned_go_init appeared on host — \
|
||||
unexpected side-effect from Docker go build"
|
||||
);
|
||||
let _ = fs::remove_file(&marker);
|
||||
}
|
||||
|
||||
/// Verify that a malicious Maven plugin (`exec-maven-plugin`) cannot write
|
||||
/// to the host when `mvn compile` runs inside the sandbox.
|
||||
/// to the host when `mvn validate` runs inside a Docker-isolated container.
|
||||
///
|
||||
/// NOTE (Phase 05): Docker + Maven compilation is deferred. `prepare_java()`
|
||||
/// runs `javac` via the process backend on the host — Docker isolation does
|
||||
/// NOT protect the build step yet.
|
||||
/// The plugin runs `echo NYX_ESCAPE_SUCCESS > /tmp/pwned_maven_plugin` during
|
||||
/// the validate phase. Inside the container, `/tmp` is private.
|
||||
///
|
||||
/// Fixture: `tests/dynamic_fixtures/escape/maven_malicious_plugin/pom.xml`
|
||||
/// Bridge networking is used so Maven can download the plugin from Maven Central.
|
||||
/// Skips gracefully when Docker is unavailable or the Maven image is not pulled.
|
||||
#[test]
|
||||
#[ignore = "Phase 06: Docker-isolated Maven build not yet implemented"]
|
||||
fn escape_maven_malicious_plugin() {
|
||||
if !docker_available() {
|
||||
return;
|
||||
}
|
||||
let marker = std::path::PathBuf::from("/tmp/pwned_maven_plugin");
|
||||
if !docker_available() { return; }
|
||||
|
||||
let tmpdir = tempfile::TempDir::new().expect("temp dir");
|
||||
let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/dynamic_fixtures/escape/maven_malicious_plugin");
|
||||
copy_dir_recursive(&fixture, tmpdir.path())
|
||||
.expect("copy maven_malicious_plugin fixture");
|
||||
|
||||
let marker: PathBuf = PathBuf::from("/tmp/pwned_maven_plugin");
|
||||
let _ = fs::remove_file(&marker);
|
||||
|
||||
// Phase 06 TODO: wire Docker-isolated mvn compile and re-enable body.
|
||||
let result =
|
||||
nyx_scanner::dynamic::build_sandbox::prepare_java_in_docker(tmpdir.path());
|
||||
if result.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
assert!(
|
||||
!marker.exists(),
|
||||
"host marker /tmp/pwned_maven_plugin must not exist before Docker+Maven build is implemented"
|
||||
"escape_maven_malicious_plugin: /tmp/pwned_maven_plugin appeared on host — \
|
||||
Docker Maven build isolation failed"
|
||||
);
|
||||
let _ = fs::remove_file(&marker);
|
||||
}
|
||||
|
||||
/// Verify that a malicious Composer `post-install-cmd` cannot write to the
|
||||
/// host when `composer install` runs inside the sandbox.
|
||||
/// host when `composer install` runs inside a Docker-isolated container.
|
||||
///
|
||||
/// NOTE (Phase 05): Docker + Composer install is deferred. `prepare_php()`
|
||||
/// runs `php` directly via the process backend — Docker isolation does NOT
|
||||
/// protect the install step yet.
|
||||
/// The script runs `echo NYX_ESCAPE_SUCCESS > /tmp/pwned_composer_postinstall`.
|
||||
/// Inside the container, `/tmp` is private; the host marker stays absent.
|
||||
///
|
||||
/// Fixture: `tests/dynamic_fixtures/escape/composer_malicious_postinstall/composer.json`
|
||||
/// Skips gracefully when Docker is unavailable or `composer:2` is not pulled.
|
||||
#[test]
|
||||
#[ignore = "Phase 06: Docker-isolated composer install not yet implemented"]
|
||||
fn escape_composer_malicious_postinstall() {
|
||||
if !docker_available() {
|
||||
return;
|
||||
}
|
||||
let marker = std::path::PathBuf::from("/tmp/pwned_composer_postinstall");
|
||||
if !docker_available() { return; }
|
||||
|
||||
let tmpdir = tempfile::TempDir::new().expect("temp dir");
|
||||
let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/dynamic_fixtures/escape/composer_malicious_postinstall");
|
||||
copy_dir_recursive(&fixture, tmpdir.path())
|
||||
.expect("copy composer_malicious_postinstall fixture");
|
||||
|
||||
let marker: PathBuf = PathBuf::from("/tmp/pwned_composer_postinstall");
|
||||
let _ = fs::remove_file(&marker);
|
||||
|
||||
// Phase 06 TODO: wire Docker-isolated composer install and re-enable body.
|
||||
let result =
|
||||
nyx_scanner::dynamic::build_sandbox::prepare_php_in_docker(tmpdir.path());
|
||||
if result.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
assert!(
|
||||
!marker.exists(),
|
||||
"host marker /tmp/pwned_composer_postinstall must not exist before Docker+Composer install is implemented"
|
||||
"escape_composer_malicious_postinstall: /tmp/pwned_composer_postinstall appeared on host — \
|
||||
Docker Composer install isolation failed"
|
||||
);
|
||||
let _ = fs::remove_file(&marker);
|
||||
}
|
||||
|
||||
// ── Positive control test ─────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue