mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0013 (20260516T052512Z-20f8)
This commit is contained in:
parent
b8207a1d1c
commit
a2cc5f7700
4 changed files with 270 additions and 7 deletions
|
|
@ -83,14 +83,22 @@ impl LangEmitter for GoEmitter {
|
|||
|
||||
/// Phase 26 — Go chain-step harness.
|
||||
///
|
||||
/// Emits a `main.go` driver that reads `NYX_PREV_OUTPUT` and forwards it
|
||||
/// on stdout. The Go probe shim (`__nyx_probe`) is top-level Go code
|
||||
/// requiring extra stdlib imports; chain steps keep the harness minimal
|
||||
/// and rely on the sandbox runner's outer probe channel to observe the
|
||||
/// final sink fire. Wiring the probe shim into chain steps is tracked
|
||||
/// alongside the Phase 15 emitter follow-up about probe shim splicing.
|
||||
/// Splices the Go probe shim ([`probe_shim`]) ahead of a minimal driver
|
||||
/// that reads `NYX_PREV_OUTPUT` and forwards it on stdout. The composite
|
||||
/// re-verifier swaps the trailing forward for the next member's
|
||||
/// payload-injection prologue when running a multi-step chain; the shim
|
||||
/// has to be in the same compilation unit so a chain step that terminates
|
||||
/// at a sink can drive the `__nyx_probe` channel directly.
|
||||
///
|
||||
/// Imports are the union of the driver imports (`fmt`, `os`) and the
|
||||
/// shim's [`SHIM_IMPORTS`], deduped + sorted so `go run step.go`
|
||||
/// compiles in a single command.
|
||||
fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
|
||||
let source = "package main\n\nimport (\n \"fmt\"\n \"os\"\n)\n\nfunc main() {\n prev := os.Getenv(\"NYX_PREV_OUTPUT\")\n fmt.Print(prev)\n}\n".to_owned();
|
||||
let imports = chain_step_imports();
|
||||
let shim = probe_shim();
|
||||
let driver =
|
||||
"func main() {\n prev := os.Getenv(\"NYX_PREV_OUTPUT\")\n fmt.Print(prev)\n}\n";
|
||||
let source = format!("package main\n\nimport (\n{imports})\n{shim}\n{driver}");
|
||||
ChainStepHarness {
|
||||
source,
|
||||
filename: "step.go".to_owned(),
|
||||
|
|
@ -106,6 +114,27 @@ fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
|
|||
}
|
||||
}
|
||||
|
||||
/// Sorted, deduped tab-prefixed import lines covering the driver's
|
||||
/// `fmt` + `os` plus everything in [`SHIM_IMPORTS`].
|
||||
fn chain_step_imports() -> String {
|
||||
let driver_imports: &[&str] = &["fmt", "os"];
|
||||
let mut all: Vec<&str> = driver_imports
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(SHIM_IMPORTS.iter().copied())
|
||||
.collect();
|
||||
all.sort_unstable();
|
||||
all.dedup();
|
||||
let mut out = String::new();
|
||||
for path in &all {
|
||||
out.push('\t');
|
||||
out.push('"');
|
||||
out.push_str(path);
|
||||
out.push_str("\"\n");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ── Phase 15: shape detector ─────────────────────────────────────────────────
|
||||
|
||||
/// Concrete per-file shape resolved by reading the entry source.
|
||||
|
|
@ -846,4 +875,42 @@ mod tests {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chain_step_splices_probe_shim_for_composite_reverify() {
|
||||
let step = chain_step(Some(b"<prev>"));
|
||||
assert!(
|
||||
step.source.contains("__nyx_probe"),
|
||||
"Go chain step must splice the probe shim"
|
||||
);
|
||||
assert!(
|
||||
step.source.starts_with("package main"),
|
||||
"Go chain step must open with package main"
|
||||
);
|
||||
assert!(
|
||||
step.source.contains("os.Getenv(\"NYX_PREV_OUTPUT\")"),
|
||||
"Go chain step must keep its NYX_PREV_OUTPUT forwarder"
|
||||
);
|
||||
let import_close = step.source.find(")\n").expect("import block must close");
|
||||
let shim_pos = step.source.find("__nyx_probe").unwrap();
|
||||
let main_pos = step.source.find("func main()").unwrap();
|
||||
assert!(
|
||||
import_close < shim_pos,
|
||||
"probe shim must come after the import block",
|
||||
);
|
||||
assert!(
|
||||
shim_pos < main_pos,
|
||||
"probe shim must come before func main() so its helpers are in scope when a sink rewrite splices in",
|
||||
);
|
||||
for path in SHIM_IMPORTS {
|
||||
let quoted = format!("\"{path}\"");
|
||||
assert!(
|
||||
step.source.contains("ed),
|
||||
"Go chain step must merge shim-required import {quoted} into its import block",
|
||||
);
|
||||
}
|
||||
// Driver imports preserved alongside the shim imports.
|
||||
assert!(step.source.contains("\"fmt\""));
|
||||
assert!(step.source.contains("\"os\""));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -332,6 +332,26 @@ function __nyx_install_crash_guard(string $sinkCallee): void {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 10 (Track D.3) stub helpers. When the verifier spawned a SqlStub it
|
||||
// publishes the queries-log path through NYX_SQL_LOG; a sink call site that
|
||||
// wants the host-side stub to see its query appends one record-per-call. The
|
||||
// helper is a no-op when NYX_SQL_LOG is unset so the same fixture source still
|
||||
// runs under harness modes that didn't spawn a stub. Mirrors the Python and
|
||||
// Node shims so the host-side SqlStub log-line format (hash-space-prefixed
|
||||
// detail lines, then the query line) is identical across language emitters.
|
||||
function __nyx_stub_sql_record($query, array $detail = []): void {
|
||||
$p = getenv('NYX_SQL_LOG');
|
||||
if ($p === false || $p === '') return;
|
||||
$buf = '';
|
||||
foreach ($detail as $k => $v) {
|
||||
$buf .= '# ' . (string)$k . ': ' . (string)$v . "\n";
|
||||
}
|
||||
$q = (string)$query;
|
||||
$buf .= $q;
|
||||
if (substr($q, -1) !== "\n") $buf .= "\n";
|
||||
@file_put_contents($p, $buf, FILE_APPEND);
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
|
|
@ -718,6 +738,19 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_shim_publishes_stub_sql_recorder() {
|
||||
let shim = probe_shim();
|
||||
assert!(
|
||||
shim.contains("function __nyx_stub_sql_record"),
|
||||
"PHP probe shim must define __nyx_stub_sql_record"
|
||||
);
|
||||
assert!(
|
||||
shim.contains("NYX_SQL_LOG"),
|
||||
"stub recorder must read NYX_SQL_LOG"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chain_step_splices_probe_shim_for_composite_reverify() {
|
||||
let step = chain_step(Some(b"<prev>"));
|
||||
|
|
|
|||
41
tests/dynamic_fixtures/stubs_e2e/php/sql/vuln/main.php
Normal file
41
tests/dynamic_fixtures/stubs_e2e/php/sql/vuln/main.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
// Phase 10 (Track D.3) stub-end-to-end fixture: PHP + SQL.
|
||||
//
|
||||
// The verifier publishes:
|
||||
//
|
||||
// NYX_SQL_ENDPOINT absolute path of a SQLite DB the SqlStub owns.
|
||||
// NYX_SQL_LOG companion log path the harness appends executed
|
||||
// queries to so the host SqlStub picks them up on
|
||||
// drain_events().
|
||||
//
|
||||
// This fixture opens the stub DB with stdlib SQLite3, runs a tautology
|
||||
// SELECT (OR 1=1), and forwards the executed query to the stub through
|
||||
// the PHP shim helper __nyx_stub_sql_record. The companion test in
|
||||
// tests/stubs_e2e_per_lang.rs splices in
|
||||
// crate::dynamic::lang::php::probe_shim ahead of this source, runs it
|
||||
// with both env vars set, and asserts the stub captured the tautology.
|
||||
|
||||
function main(): void {
|
||||
$db_path = getenv('NYX_SQL_ENDPOINT');
|
||||
if ($db_path === false || $db_path === '') {
|
||||
return;
|
||||
}
|
||||
$query = "SELECT 1 WHERE 'a' = 'a' OR 1=1 --";
|
||||
$driver = 'none';
|
||||
if (class_exists('SQLite3')) {
|
||||
$driver = 'SQLite3';
|
||||
$db = new SQLite3($db_path);
|
||||
$rows = $db->query($query);
|
||||
if ($rows !== false) {
|
||||
while ($r = $rows->fetchArray(SQLITE3_NUM)) {
|
||||
echo $r[0] . "\n";
|
||||
}
|
||||
}
|
||||
$db->close();
|
||||
}
|
||||
// Record the executed query through the probe shim so the host
|
||||
// SqlStub captures it on the next drain_events() call.
|
||||
__nyx_stub_sql_record($query, ['driver' => $driver]);
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
#![cfg(feature = "dynamic")]
|
||||
|
||||
use nyx_scanner::dynamic::lang::javascript::probe_shim as node_probe_shim;
|
||||
use nyx_scanner::dynamic::lang::php::probe_shim as php_probe_shim;
|
||||
use nyx_scanner::dynamic::lang::python::probe_shim as python_probe_shim;
|
||||
use nyx_scanner::dynamic::stubs::{SqlStub, StubProvider};
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -43,6 +44,14 @@ fn node_available() -> bool {
|
|||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn php_available() -> bool {
|
||||
Command::new("php")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn fixture_path(rel: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests")
|
||||
|
|
@ -212,6 +221,119 @@ fn node_sql_stub_captures_tautology_query_via_shim_recorder() {
|
|||
);
|
||||
}
|
||||
|
||||
fn strip_php_open_tag(src: &str) -> &str {
|
||||
src.strip_prefix("<?php\n")
|
||||
.or_else(|| src.strip_prefix("<?php\r\n"))
|
||||
.or_else(|| src.strip_prefix("<?php "))
|
||||
.unwrap_or(src)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_sql_stub_captures_tautology_query_via_shim_recorder() {
|
||||
if !php_available() {
|
||||
eprintln!("SKIP: php not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = SqlStub::start(workdir.path()).expect("SqlStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let recording = stub
|
||||
.recording_endpoint()
|
||||
.expect("SqlStub must publish a recording endpoint");
|
||||
|
||||
// Splice the PHP probe shim ahead of the fixture source so the
|
||||
// generated program carries the `__nyx_stub_sql_record` helper.
|
||||
// Mirrors the production `PhpEmitter::emit` ordering. The shim
|
||||
// expects to live inside an open `<?php` block, so we strip the
|
||||
// fixture's leading `<?php` tag before concatenating.
|
||||
let fixture =
|
||||
std::fs::read_to_string(fixture_path("php/sql/vuln/main.php")).expect("read fixture");
|
||||
let body = strip_php_open_tag(&fixture);
|
||||
let mut combined = String::with_capacity(php_probe_shim().len() + body.len() + 64);
|
||||
combined.push_str("<?php\n");
|
||||
combined.push_str(php_probe_shim());
|
||||
combined.push_str("\n// ── fixture begins ─\n");
|
||||
combined.push_str(body);
|
||||
|
||||
let script_path = workdir.path().join("driver.php");
|
||||
std::fs::write(&script_path, combined).expect("write driver");
|
||||
|
||||
let output = Command::new("php")
|
||||
.arg(&script_path)
|
||||
.env("NYX_SQL_ENDPOINT", &endpoint)
|
||||
.env(recording.0, &recording.1)
|
||||
.output()
|
||||
.expect("php driver");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0; stderr = {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let events = stub.drain_events();
|
||||
assert!(
|
||||
!events.is_empty(),
|
||||
"SqlStub must capture at least one event after the PHP shim recorder fires"
|
||||
);
|
||||
let tautology = events
|
||||
.iter()
|
||||
.find(|e| e.summary.contains("OR 1=1"))
|
||||
.expect("recorded query must contain the tautology marker");
|
||||
let driver = tautology
|
||||
.detail
|
||||
.get("driver")
|
||||
.map(String::as_str)
|
||||
.expect("PHP shim must publish driver detail on the recorded event");
|
||||
assert!(
|
||||
driver == "SQLite3" || driver == "none",
|
||||
"driver detail must report SQLite3 when the stdlib class is available or `none` when missing; got {driver:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_sql_shim_recorder_is_noop_without_log_env() {
|
||||
if !php_available() {
|
||||
eprintln!("SKIP: php not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = SqlStub::start(workdir.path()).expect("SqlStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let fixture =
|
||||
std::fs::read_to_string(fixture_path("php/sql/vuln/main.php")).expect("read fixture");
|
||||
let body = strip_php_open_tag(&fixture);
|
||||
let mut combined = String::new();
|
||||
combined.push_str("<?php\n");
|
||||
combined.push_str(php_probe_shim());
|
||||
combined.push('\n');
|
||||
combined.push_str(body);
|
||||
let script_path = workdir.path().join("driver_no_log.php");
|
||||
std::fs::write(&script_path, combined).expect("write driver");
|
||||
|
||||
let output = Command::new("php")
|
||||
.arg(&script_path)
|
||||
.env("NYX_SQL_ENDPOINT", &endpoint)
|
||||
.env_remove("NYX_SQL_LOG")
|
||||
.output()
|
||||
.expect("php driver");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0 even without NYX_SQL_LOG; stderr = {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let events = stub.drain_events();
|
||||
assert!(
|
||||
events.is_empty(),
|
||||
"no events expected when the recording env var is unset, got {} entries",
|
||||
events.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_sql_shim_recorder_is_noop_without_log_env() {
|
||||
if !node_available() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue