mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-12 19:55:14 +02:00
[pitboss/grind] deferred session-0014 (20260516T052512Z-20f8)
This commit is contained in:
parent
a2cc5f7700
commit
6a169f51b8
23 changed files with 737 additions and 29 deletions
|
|
@ -141,6 +141,29 @@ def __nyx_stub_sql_record(query, **detail):
|
|||
except OSError:
|
||||
pass
|
||||
|
||||
# Phase 10 (Track D.3) HTTP recording helper. When the verifier spawned an
|
||||
# HttpStub it publishes the side-channel log path through NYX_HTTP_LOG; a
|
||||
# sink call site whose outbound request never reaches the on-the-wire
|
||||
# listener (DNS-mocked, network-isolated sandbox, pre-flight check) can
|
||||
# call this helper to surface the attempted call. Format matches the SQL
|
||||
# helper so the host-side merger parses both streams identically.
|
||||
def __nyx_stub_http_record(method, url, body=None, **detail):
|
||||
import os
|
||||
p = os.environ.get("NYX_HTTP_LOG")
|
||||
if not p:
|
||||
return
|
||||
try:
|
||||
with open(p, "a") as _f:
|
||||
_f.write('# method: %s\n' % str(method))
|
||||
_f.write('# url: %s\n' % str(url))
|
||||
if body is not None:
|
||||
_f.write('# body: %s\n' % str(body))
|
||||
for k, v in detail.items():
|
||||
_f.write('# %s: %s\n' % (str(k), str(v)))
|
||||
_f.write('%s %s\n' % (str(method), str(url)))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
_NYX_SINK_FILE = "<TMPDIR>/<ENTRY_FILE>"
|
||||
_NYX_SINK_LINE = 13
|
||||
|
|
|
|||
|
|
@ -141,6 +141,29 @@ def __nyx_stub_sql_record(query, **detail):
|
|||
except OSError:
|
||||
pass
|
||||
|
||||
# Phase 10 (Track D.3) HTTP recording helper. When the verifier spawned an
|
||||
# HttpStub it publishes the side-channel log path through NYX_HTTP_LOG; a
|
||||
# sink call site whose outbound request never reaches the on-the-wire
|
||||
# listener (DNS-mocked, network-isolated sandbox, pre-flight check) can
|
||||
# call this helper to surface the attempted call. Format matches the SQL
|
||||
# helper so the host-side merger parses both streams identically.
|
||||
def __nyx_stub_http_record(method, url, body=None, **detail):
|
||||
import os
|
||||
p = os.environ.get("NYX_HTTP_LOG")
|
||||
if not p:
|
||||
return
|
||||
try:
|
||||
with open(p, "a") as _f:
|
||||
_f.write('# method: %s\n' % str(method))
|
||||
_f.write('# url: %s\n' % str(url))
|
||||
if body is not None:
|
||||
_f.write('# body: %s\n' % str(body))
|
||||
for k, v in detail.items():
|
||||
_f.write('# %s: %s\n' % (str(k), str(v)))
|
||||
_f.write('%s %s\n' % (str(method), str(url)))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
_NYX_SINK_FILE = "<TMPDIR>/<ENTRY_FILE>"
|
||||
_NYX_SINK_LINE = 17
|
||||
|
|
|
|||
|
|
@ -141,6 +141,29 @@ def __nyx_stub_sql_record(query, **detail):
|
|||
except OSError:
|
||||
pass
|
||||
|
||||
# Phase 10 (Track D.3) HTTP recording helper. When the verifier spawned an
|
||||
# HttpStub it publishes the side-channel log path through NYX_HTTP_LOG; a
|
||||
# sink call site whose outbound request never reaches the on-the-wire
|
||||
# listener (DNS-mocked, network-isolated sandbox, pre-flight check) can
|
||||
# call this helper to surface the attempted call. Format matches the SQL
|
||||
# helper so the host-side merger parses both streams identically.
|
||||
def __nyx_stub_http_record(method, url, body=None, **detail):
|
||||
import os
|
||||
p = os.environ.get("NYX_HTTP_LOG")
|
||||
if not p:
|
||||
return
|
||||
try:
|
||||
with open(p, "a") as _f:
|
||||
_f.write('# method: %s\n' % str(method))
|
||||
_f.write('# url: %s\n' % str(url))
|
||||
if body is not None:
|
||||
_f.write('# body: %s\n' % str(body))
|
||||
for k, v in detail.items():
|
||||
_f.write('# %s: %s\n' % (str(k), str(v)))
|
||||
_f.write('%s %s\n' % (str(method), str(url)))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
_NYX_SINK_FILE = "<TMPDIR>/<ENTRY_FILE>"
|
||||
_NYX_SINK_LINE = 14
|
||||
|
|
|
|||
|
|
@ -141,6 +141,29 @@ def __nyx_stub_sql_record(query, **detail):
|
|||
except OSError:
|
||||
pass
|
||||
|
||||
# Phase 10 (Track D.3) HTTP recording helper. When the verifier spawned an
|
||||
# HttpStub it publishes the side-channel log path through NYX_HTTP_LOG; a
|
||||
# sink call site whose outbound request never reaches the on-the-wire
|
||||
# listener (DNS-mocked, network-isolated sandbox, pre-flight check) can
|
||||
# call this helper to surface the attempted call. Format matches the SQL
|
||||
# helper so the host-side merger parses both streams identically.
|
||||
def __nyx_stub_http_record(method, url, body=None, **detail):
|
||||
import os
|
||||
p = os.environ.get("NYX_HTTP_LOG")
|
||||
if not p:
|
||||
return
|
||||
try:
|
||||
with open(p, "a") as _f:
|
||||
_f.write('# method: %s\n' % str(method))
|
||||
_f.write('# url: %s\n' % str(url))
|
||||
if body is not None:
|
||||
_f.write('# body: %s\n' % str(body))
|
||||
for k, v in detail.items():
|
||||
_f.write('# %s: %s\n' % (str(k), str(v)))
|
||||
_f.write('%s %s\n' % (str(method), str(url)))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
_NYX_SINK_FILE = "<TMPDIR>/<ENTRY_FILE>"
|
||||
_NYX_SINK_LINE = 15
|
||||
|
|
|
|||
|
|
@ -141,6 +141,29 @@ def __nyx_stub_sql_record(query, **detail):
|
|||
except OSError:
|
||||
pass
|
||||
|
||||
# Phase 10 (Track D.3) HTTP recording helper. When the verifier spawned an
|
||||
# HttpStub it publishes the side-channel log path through NYX_HTTP_LOG; a
|
||||
# sink call site whose outbound request never reaches the on-the-wire
|
||||
# listener (DNS-mocked, network-isolated sandbox, pre-flight check) can
|
||||
# call this helper to surface the attempted call. Format matches the SQL
|
||||
# helper so the host-side merger parses both streams identically.
|
||||
def __nyx_stub_http_record(method, url, body=None, **detail):
|
||||
import os
|
||||
p = os.environ.get("NYX_HTTP_LOG")
|
||||
if not p:
|
||||
return
|
||||
try:
|
||||
with open(p, "a") as _f:
|
||||
_f.write('# method: %s\n' % str(method))
|
||||
_f.write('# url: %s\n' % str(url))
|
||||
if body is not None:
|
||||
_f.write('# body: %s\n' % str(body))
|
||||
for k, v in detail.items():
|
||||
_f.write('# %s: %s\n' % (str(k), str(v)))
|
||||
_f.write('%s %s\n' % (str(method), str(url)))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
_NYX_SINK_FILE = "<TMPDIR>/<ENTRY_FILE>"
|
||||
_NYX_SINK_LINE = 16
|
||||
|
|
|
|||
|
|
@ -141,6 +141,29 @@ def __nyx_stub_sql_record(query, **detail):
|
|||
except OSError:
|
||||
pass
|
||||
|
||||
# Phase 10 (Track D.3) HTTP recording helper. When the verifier spawned an
|
||||
# HttpStub it publishes the side-channel log path through NYX_HTTP_LOG; a
|
||||
# sink call site whose outbound request never reaches the on-the-wire
|
||||
# listener (DNS-mocked, network-isolated sandbox, pre-flight check) can
|
||||
# call this helper to surface the attempted call. Format matches the SQL
|
||||
# helper so the host-side merger parses both streams identically.
|
||||
def __nyx_stub_http_record(method, url, body=None, **detail):
|
||||
import os
|
||||
p = os.environ.get("NYX_HTTP_LOG")
|
||||
if not p:
|
||||
return
|
||||
try:
|
||||
with open(p, "a") as _f:
|
||||
_f.write('# method: %s\n' % str(method))
|
||||
_f.write('# url: %s\n' % str(url))
|
||||
if body is not None:
|
||||
_f.write('# body: %s\n' % str(body))
|
||||
for k, v in detail.items():
|
||||
_f.write('# %s: %s\n' % (str(k), str(v)))
|
||||
_f.write('%s %s\n' % (str(method), str(url)))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
_NYX_SINK_FILE = "<TMPDIR>/<ENTRY_FILE>"
|
||||
_NYX_SINK_LINE = 18
|
||||
|
|
|
|||
|
|
@ -141,6 +141,29 @@ def __nyx_stub_sql_record(query, **detail):
|
|||
except OSError:
|
||||
pass
|
||||
|
||||
# Phase 10 (Track D.3) HTTP recording helper. When the verifier spawned an
|
||||
# HttpStub it publishes the side-channel log path through NYX_HTTP_LOG; a
|
||||
# sink call site whose outbound request never reaches the on-the-wire
|
||||
# listener (DNS-mocked, network-isolated sandbox, pre-flight check) can
|
||||
# call this helper to surface the attempted call. Format matches the SQL
|
||||
# helper so the host-side merger parses both streams identically.
|
||||
def __nyx_stub_http_record(method, url, body=None, **detail):
|
||||
import os
|
||||
p = os.environ.get("NYX_HTTP_LOG")
|
||||
if not p:
|
||||
return
|
||||
try:
|
||||
with open(p, "a") as _f:
|
||||
_f.write('# method: %s\n' % str(method))
|
||||
_f.write('# url: %s\n' % str(url))
|
||||
if body is not None:
|
||||
_f.write('# body: %s\n' % str(body))
|
||||
for k, v in detail.items():
|
||||
_f.write('# %s: %s\n' % (str(k), str(v)))
|
||||
_f.write('%s %s\n' % (str(method), str(url)))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
_NYX_SINK_FILE = "<TMPDIR>/<ENTRY_FILE>"
|
||||
_NYX_SINK_LINE = 12
|
||||
|
|
|
|||
|
|
@ -141,6 +141,29 @@ def __nyx_stub_sql_record(query, **detail):
|
|||
except OSError:
|
||||
pass
|
||||
|
||||
# Phase 10 (Track D.3) HTTP recording helper. When the verifier spawned an
|
||||
# HttpStub it publishes the side-channel log path through NYX_HTTP_LOG; a
|
||||
# sink call site whose outbound request never reaches the on-the-wire
|
||||
# listener (DNS-mocked, network-isolated sandbox, pre-flight check) can
|
||||
# call this helper to surface the attempted call. Format matches the SQL
|
||||
# helper so the host-side merger parses both streams identically.
|
||||
def __nyx_stub_http_record(method, url, body=None, **detail):
|
||||
import os
|
||||
p = os.environ.get("NYX_HTTP_LOG")
|
||||
if not p:
|
||||
return
|
||||
try:
|
||||
with open(p, "a") as _f:
|
||||
_f.write('# method: %s\n' % str(method))
|
||||
_f.write('# url: %s\n' % str(url))
|
||||
if body is not None:
|
||||
_f.write('# body: %s\n' % str(body))
|
||||
for k, v in detail.items():
|
||||
_f.write('# %s: %s\n' % (str(k), str(v)))
|
||||
_f.write('%s %s\n' % (str(method), str(url)))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
_NYX_SINK_FILE = "<TMPDIR>/<ENTRY_FILE>"
|
||||
_NYX_SINK_LINE = 14
|
||||
|
|
|
|||
36
tests/dynamic_fixtures/stubs_e2e/python/http/vuln/main.py
Normal file
36
tests/dynamic_fixtures/stubs_e2e/python/http/vuln/main.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
"""Phase 10 (Track D.3) stub-end-to-end fixture: Python + HTTP.
|
||||
|
||||
The verifier publishes:
|
||||
|
||||
* ``NYX_HTTP_ENDPOINT`` — `http://127.0.0.1:{port}` the HttpStub listens on.
|
||||
* ``NYX_HTTP_LOG`` — companion log path the harness appends attempted
|
||||
outbound calls to so the host HttpStub picks them up on
|
||||
``drain_events()`` even when the request bypasses the on-the-wire
|
||||
listener (DNS-mocked, network-isolated sandbox, pre-flight check).
|
||||
|
||||
This fixture exercises the side-channel path: it records an attempted
|
||||
SSRF call to ``http://169.254.169.254/latest/meta-data/`` through the
|
||||
Python shim helper ``__nyx_stub_http_record`` without issuing the
|
||||
actual network call. The companion test in
|
||||
``tests/stubs_e2e_per_lang.rs`` splices in
|
||||
``crate::dynamic::lang::python::probe_shim`` ahead of this source, runs
|
||||
it with both env vars set, and asserts the stub captured the attempt.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def main():
|
||||
method = "GET"
|
||||
url = "http://169.254.169.254/latest/meta-data/"
|
||||
body = ""
|
||||
# Record the attempted call through the probe shim so the host
|
||||
# HttpStub captures it on the next drain_events() call even when
|
||||
# the harness never reaches the on-the-wire listener.
|
||||
__nyx_stub_http_record(method, url, body, driver="urllib")
|
||||
# Echo so the host can confirm the driver ran end-to-end.
|
||||
print(os.environ.get("NYX_HTTP_ENDPOINT", "no-endpoint"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
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 nyx_scanner::dynamic::stubs::{HttpStub, SqlStub, StubProvider};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
|
|
@ -334,6 +334,113 @@ fn php_sql_shim_recorder_is_noop_without_log_env() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_http_stub_captures_attempted_outbound_via_shim_recorder() {
|
||||
// Phase 10 (Track D.3) HTTP recording: the side-channel
|
||||
// `__nyx_stub_http_record` lets a harness surface outbound HTTP
|
||||
// attempts even when the request never reaches the on-the-wire
|
||||
// listener (DNS-mocked, network-isolated sandbox, pre-flight
|
||||
// check). This test drives the Python helper.
|
||||
if !python3_available() {
|
||||
eprintln!("SKIP: python3 not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = HttpStub::start(workdir.path()).expect("HttpStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let recording = stub
|
||||
.recording_endpoint()
|
||||
.expect("HttpStub must publish a recording endpoint");
|
||||
|
||||
let fixture =
|
||||
std::fs::read_to_string(fixture_path("python/http/vuln/main.py")).expect("read fixture");
|
||||
let mut combined = String::with_capacity(python_probe_shim().len() + fixture.len() + 64);
|
||||
combined.push_str(python_probe_shim());
|
||||
combined.push_str("\n# ── fixture begins ─\n");
|
||||
combined.push_str(&fixture);
|
||||
|
||||
let script_path = workdir.path().join("driver_http.py");
|
||||
std::fs::write(&script_path, combined).expect("write driver");
|
||||
|
||||
let output = Command::new("python3")
|
||||
.arg(&script_path)
|
||||
.env("NYX_HTTP_ENDPOINT", &endpoint)
|
||||
.env(recording.0, &recording.1)
|
||||
.output()
|
||||
.expect("python3 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(),
|
||||
"HttpStub must capture at least one event after the shim recorder fires"
|
||||
);
|
||||
let hit = events
|
||||
.iter()
|
||||
.find(|e| e.summary.contains("169.254.169.254"))
|
||||
.expect("recorded URL must contain the SSRF marker");
|
||||
assert_eq!(
|
||||
hit.detail.get("method").map(String::as_str),
|
||||
Some("GET"),
|
||||
"method detail must surface on the recorded event"
|
||||
);
|
||||
assert_eq!(
|
||||
hit.detail.get("url").map(String::as_str),
|
||||
Some("http://169.254.169.254/latest/meta-data/"),
|
||||
);
|
||||
assert_eq!(
|
||||
hit.detail.get("driver").map(String::as_str),
|
||||
Some("urllib"),
|
||||
"kwargs passed to __nyx_stub_http_record must surface as event detail entries"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_http_shim_recorder_is_noop_without_log_env() {
|
||||
if !python3_available() {
|
||||
eprintln!("SKIP: python3 not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = HttpStub::start(workdir.path()).expect("HttpStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let fixture =
|
||||
std::fs::read_to_string(fixture_path("python/http/vuln/main.py")).expect("read fixture");
|
||||
let mut combined = String::new();
|
||||
combined.push_str(python_probe_shim());
|
||||
combined.push('\n');
|
||||
combined.push_str(&fixture);
|
||||
let script_path = workdir.path().join("driver_http_no_log.py");
|
||||
std::fs::write(&script_path, combined).expect("write driver");
|
||||
|
||||
let output = Command::new("python3")
|
||||
.arg(&script_path)
|
||||
.env("NYX_HTTP_ENDPOINT", &endpoint)
|
||||
.env_remove("NYX_HTTP_LOG")
|
||||
.output()
|
||||
.expect("python3 driver");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0 even without NYX_HTTP_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() {
|
||||
|
|
|
|||
|
|
@ -159,7 +159,8 @@ fn sql_stub_captured_query_threads_through_probe_predicate() {
|
|||
|
||||
#[test]
|
||||
fn http_stub_vuln_fixture_confirms_recorded_request() {
|
||||
let stub = HttpStub::start().unwrap();
|
||||
let workdir = TempDir::new().unwrap();
|
||||
let stub = HttpStub::start(workdir.path()).unwrap();
|
||||
let payload = extract_payload(&read_fixture("http", "vuln.txt"));
|
||||
assert!(payload.contains("169.254"), "vuln fixture must carry metadata host");
|
||||
|
||||
|
|
@ -177,7 +178,8 @@ fn http_stub_vuln_fixture_confirms_recorded_request() {
|
|||
|
||||
#[test]
|
||||
fn http_stub_benign_fixture_does_not_confirm() {
|
||||
let stub = HttpStub::start().unwrap();
|
||||
let workdir = TempDir::new().unwrap();
|
||||
let stub = HttpStub::start(workdir.path()).unwrap();
|
||||
let payload = extract_payload(&read_fixture("http", "benign.txt"));
|
||||
stub.record(payload);
|
||||
let events = stub.drain_events();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue