[pitboss/grind] deferred session-0013 (20260516T052512Z-20f8)

This commit is contained in:
pitboss 2026-05-16 07:53:03 -05:00
parent b8207a1d1c
commit a2cc5f7700
4 changed files with 270 additions and 7 deletions

View 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();

View file

@ -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() {