mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0012 (20260516T052512Z-20f8)
This commit is contained in:
parent
d126f3c15c
commit
b8207a1d1c
4 changed files with 243 additions and 6 deletions
|
|
@ -85,13 +85,21 @@ impl LangEmitter for JavaEmitter {
|
|||
/// Emits a `Step.java` class whose `main` reads `NYX_PREV_OUTPUT` and
|
||||
/// forwards it on stdout. The command shell-wraps `javac` + `java` so
|
||||
/// the step actually runs after the build step completes (the
|
||||
/// `ChainStepHarness.command` slot models a single process). The Java
|
||||
/// probe shim is class-level and requires `System` / `java.io.*` imports
|
||||
/// the chain step already pulls in implicitly; wiring the full shim is
|
||||
/// tracked alongside the Phase 14 emitter follow-up about probe shim
|
||||
/// splicing.
|
||||
/// `ChainStepHarness.command` slot models a single process).
|
||||
///
|
||||
/// The Java probe shim (`__nyx_probe`, `__nyx_install_crash_guard`,
|
||||
/// helpers) is spliced as class-member declarations inside `class Step
|
||||
/// { … }` between the class-open brace and `public static void main`,
|
||||
/// so a downstream sink rewrite within the step body has the shim
|
||||
/// helpers already in scope. The shim uses only `java.lang.*` plus
|
||||
/// fully-qualified `java.util.TreeMap` / `java.io.FileWriter` /
|
||||
/// `java.nio.charset.StandardCharsets`, so no extra `import` lines
|
||||
/// are needed beyond what stock Java implicitly imports.
|
||||
fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
|
||||
let source = "public class Step {\n public static void main(String[] args) {\n String prev = System.getenv(\"NYX_PREV_OUTPUT\");\n if (prev == null) prev = \"\";\n System.out.print(prev);\n }\n}\n".to_owned();
|
||||
let shim = probe_shim();
|
||||
let source = format!(
|
||||
"public class Step {{\n{shim}\n public static void main(String[] args) {{\n String prev = System.getenv(\"NYX_PREV_OUTPUT\");\n if (prev == null) prev = \"\";\n System.out.print(prev);\n }}\n}}\n"
|
||||
);
|
||||
ChainStepHarness {
|
||||
source,
|
||||
filename: "Step.java".to_owned(),
|
||||
|
|
@ -1031,6 +1039,35 @@ mod tests {
|
|||
assert_eq!(harness.entry_subpath, Some("Entry.java".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chain_step_splices_probe_shim_for_composite_reverify() {
|
||||
let step = chain_step(Some(b"<prev>"));
|
||||
assert!(
|
||||
step.source.contains("__nyx_probe"),
|
||||
"Java chain step must splice the probe shim"
|
||||
);
|
||||
assert!(
|
||||
step.source.starts_with("public class Step {"),
|
||||
"Java chain step must open with the `public class Step {{` declaration"
|
||||
);
|
||||
assert!(
|
||||
step.source.contains("System.getenv(\"NYX_PREV_OUTPUT\")"),
|
||||
"Java chain step must keep its NYX_PREV_OUTPUT forwarder"
|
||||
);
|
||||
let shim_pos = step.source.find("__nyx_probe").unwrap();
|
||||
let driver_pos = step.source.find("System.getenv(\"NYX_PREV_OUTPUT\")").unwrap();
|
||||
assert!(
|
||||
shim_pos < driver_pos,
|
||||
"probe shim must come before the driver so the shim's helpers are in scope when a sink rewrite splices in"
|
||||
);
|
||||
let main_pos = step.source.find("public static void main").unwrap();
|
||||
assert!(
|
||||
shim_pos < main_pos,
|
||||
"probe shim members must be declared before `main` so the class compiles cleanly"
|
||||
);
|
||||
assert_eq!(step.filename, "Step.java");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_shape_reads_file_and_returns_shape() {
|
||||
// Drive the public `detect_shape(spec)` wrapper end-to-end:
|
||||
|
|
|
|||
|
|
@ -250,6 +250,34 @@ function __nyx_install_crash_guard(sinkCallee) {
|
|||
} catch (e) { /* runtime refused signal handler */ }
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// shim's __nyx_stub_sql_record so the host-side SqlStub log-line format
|
||||
// (key/value detail lines prefixed with hash-space, followed by the query
|
||||
// line) is identical across language emitters.
|
||||
function __nyx_stub_sql_record(query, detail) {
|
||||
const _p = process.env.NYX_SQL_LOG;
|
||||
if (!_p) return;
|
||||
const _fs = require('fs');
|
||||
try {
|
||||
let _buf = '';
|
||||
if (detail && typeof detail === 'object') {
|
||||
for (const _k of Object.keys(detail)) {
|
||||
_buf += '# ' + String(_k) + ': ' + String(detail[_k]) + '\n';
|
||||
}
|
||||
}
|
||||
const _q = String(query);
|
||||
_buf += _q;
|
||||
if (!_q.endsWith('\n')) _buf += '\n';
|
||||
_fs.appendFileSync(_p, _buf);
|
||||
} catch (e) {
|
||||
// best-effort: stub recorder write failure is non-fatal.
|
||||
}
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
|
|
@ -1029,4 +1057,21 @@ mod tests {
|
|||
assert_eq!(h_js.entry_subpath, h_ts.entry_subpath);
|
||||
assert_eq!(h_js.entry_subpath.as_deref(), Some("entry.js"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_shim_publishes_stub_sql_recorder() {
|
||||
let shim = probe_shim();
|
||||
assert!(
|
||||
shim.contains("function __nyx_stub_sql_record"),
|
||||
"Node probe shim must define __nyx_stub_sql_record"
|
||||
);
|
||||
assert!(
|
||||
shim.contains("NYX_SQL_LOG"),
|
||||
"stub recorder must read NYX_SQL_LOG"
|
||||
);
|
||||
assert!(
|
||||
shim.contains("appendFileSync"),
|
||||
"stub recorder must append to the log file"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
46
tests/dynamic_fixtures/stubs_e2e/node/sql/vuln/main.js
Normal file
46
tests/dynamic_fixtures/stubs_e2e/node/sql/vuln/main.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// Phase 10 (Track D.3) stub-end-to-end fixture: Node + 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 mirrors the Python sibling at
|
||||
// tests/dynamic_fixtures/stubs_e2e/python/sql/vuln/main.py. It opens
|
||||
// the stub DB through Node's experimental stdlib `node:sqlite` module
|
||||
// (Node 22.5+), runs a tautology SELECT (OR 1=1), and forwards the
|
||||
// executed query to the stub through the JS shim helper
|
||||
// `__nyx_stub_sql_record`. When `node:sqlite` is missing (older Node
|
||||
// or stripped runtimes) the DB exec step is skipped but the shim
|
||||
// recorder still fires so the stub captures the query regardless.
|
||||
|
||||
'use strict';
|
||||
|
||||
function main() {
|
||||
const dbPath = process.env.NYX_SQL_ENDPOINT;
|
||||
if (!dbPath) return;
|
||||
const query = "SELECT 1 WHERE 'a' = 'a' OR 1=1 --";
|
||||
|
||||
let driverName = 'none';
|
||||
try {
|
||||
const sqlite = require('node:sqlite');
|
||||
const db = new sqlite.DatabaseSync(dbPath);
|
||||
try {
|
||||
const rows = db.prepare(query).all();
|
||||
for (const row of rows) {
|
||||
process.stdout.write(String(Object.values(row)[0]) + '\n');
|
||||
}
|
||||
driverName = 'node:sqlite';
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
} catch (e) {
|
||||
// node:sqlite unavailable on this Node version; skip the
|
||||
// exec but still record the query so the stub sees the call.
|
||||
}
|
||||
|
||||
__nyx_stub_sql_record(query, { driver: driverName });
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
use nyx_scanner::dynamic::lang::javascript::probe_shim as node_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;
|
||||
|
|
@ -34,6 +35,14 @@ fn python3_available() -> bool {
|
|||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn node_available() -> bool {
|
||||
Command::new("node")
|
||||
.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")
|
||||
|
|
@ -142,3 +151,103 @@ fn python_sql_shim_recorder_is_noop_without_log_env() {
|
|||
events.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_sql_stub_captures_tautology_query_via_shim_recorder() {
|
||||
if !node_available() {
|
||||
eprintln!("SKIP: node 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 Node probe shim ahead of the fixture source so the
|
||||
// generated program carries the `__nyx_stub_sql_record` helper.
|
||||
// Mirrors the production `JavaScriptEmitter::emit` ordering.
|
||||
let fixture =
|
||||
std::fs::read_to_string(fixture_path("node/sql/vuln/main.js")).expect("read fixture");
|
||||
let mut combined = String::with_capacity(node_probe_shim().len() + fixture.len() + 64);
|
||||
combined.push_str(node_probe_shim());
|
||||
combined.push_str("\n// ── fixture begins ─\n");
|
||||
combined.push_str(&fixture);
|
||||
|
||||
let script_path = workdir.path().join("driver.js");
|
||||
std::fs::write(&script_path, combined).expect("write driver");
|
||||
|
||||
let output = Command::new("node")
|
||||
.arg(&script_path)
|
||||
.env("NYX_SQL_ENDPOINT", &endpoint)
|
||||
.env(recording.0, &recording.1)
|
||||
.output()
|
||||
.expect("node 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 Node 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("Node shim must publish driver detail on the recorded event");
|
||||
assert!(
|
||||
driver == "node:sqlite" || driver == "none",
|
||||
"driver detail must report node:sqlite when available or `none` when the stdlib module is missing; got {driver:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_sql_shim_recorder_is_noop_without_log_env() {
|
||||
if !node_available() {
|
||||
eprintln!("SKIP: node 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("node/sql/vuln/main.js")).expect("read fixture");
|
||||
let mut combined = String::new();
|
||||
combined.push_str(node_probe_shim());
|
||||
combined.push('\n');
|
||||
combined.push_str(&fixture);
|
||||
let script_path = workdir.path().join("driver_no_log.js");
|
||||
std::fs::write(&script_path, combined).expect("write driver");
|
||||
|
||||
let output = Command::new("node")
|
||||
.arg(&script_path)
|
||||
.env("NYX_SQL_ENDPOINT", &endpoint)
|
||||
.env_remove("NYX_SQL_LOG")
|
||||
.output()
|
||||
.expect("node 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()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue