mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0023 (20260522T043516Z-29b8)
This commit is contained in:
parent
e9c35150de
commit
e44f6ee1af
4 changed files with 558 additions and 4 deletions
|
|
@ -54,4 +54,63 @@ pub const PAYLOADS: &[CuratedPayload] = &[
|
|||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
// Phase 08 tier-(b): raw-socket wire-frame smuggling payload.
|
||||
// Same CRLF-bearing bytes as the axum payload above, but pinned to
|
||||
// the `rust_raw` fixture (a `std::net::TcpListener` driven by
|
||||
// `create_server` + `run_once` that writes raw bytes via
|
||||
// `TcpStream::write_all`). The wire frame captured off the
|
||||
// response socket carries two distinct `Set-Cookie:` lines, so
|
||||
// `HeaderSmuggledInWire { primary: "Set-Cookie", smuggled:
|
||||
// "Set-Cookie" }` fires — proving the smuggled header survived to
|
||||
// the actual wire instead of being CRLF-stripped en route.
|
||||
//
|
||||
// Distinct payload (not just an extra predicate on the axum row)
|
||||
// because every framework's response serializer strips CRLF at
|
||||
// the wire-write boundary, so the wire-frame predicate would
|
||||
// never fire against the canonical axum fixture. See
|
||||
// `.pitboss/play/deferred.md` (Phase 08 wire-frame option A) for
|
||||
// the framework-level CRLF-strip empirical from session-0018.
|
||||
CuratedPayload {
|
||||
bytes: b"nyx-session\r\nSet-Cookie: nyx-injected=pwn",
|
||||
label: "header-injection-rust-raw-wire-smuggle",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::HeaderSmuggledInWire {
|
||||
primary: "Set-Cookie",
|
||||
smuggled: "Set-Cookie",
|
||||
}],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 12,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/header_injection/rust_raw/vuln.rs"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::HeaderSmuggledInWire {
|
||||
primary: "Set-Cookie",
|
||||
smuggled: "Set-Cookie",
|
||||
}],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "header-injection-rust-raw-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"nyx-session%0D%0ASet-Cookie%3A%20nyx-injected%3Dpwn",
|
||||
label: "header-injection-rust-raw-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::HeaderSmuggledInWire {
|
||||
primary: "Set-Cookie",
|
||||
smuggled: "Set-Cookie",
|
||||
}],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 12,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/header_injection/rust_raw/vuln.rs"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -629,12 +629,25 @@ pub fn detect_shape(spec: &HarnessSpec) -> RustShape {
|
|||
/// build would panic on the vuln payload before the differential
|
||||
/// oracle sees the smuggled header).
|
||||
///
|
||||
/// Tier (b): when the fixture does not import axum, fall back to the
|
||||
/// synthetic `nyx_header_probe("Set-Cookie", &payload)` call so the
|
||||
/// differential oracle still flips on raw payload bytes.
|
||||
/// Tier (b) — raw-socket wire frame: when the fixture uses
|
||||
/// `std::net::TcpListener::bind` (the `rust_raw` fixture exports
|
||||
/// `create_server` + `run_once` + `set_cookie_value`), boot the
|
||||
/// listener on a loopback port via the fixture, open a `TcpStream`
|
||||
/// from the harness, read the bytes the fixture wrote to the response
|
||||
/// socket up to the `\r\n\r\n` boundary, and emit them as a
|
||||
/// `ProbeKind::HeaderWireFrame` record. Bypasses every framework-
|
||||
/// level CRLF validator since the fixture owns the write path.
|
||||
///
|
||||
/// Tier (c) synthetic fallback: when the fixture imports neither
|
||||
/// axum nor TcpListener, fall back to the synthetic
|
||||
/// `nyx_header_probe("Set-Cookie", &payload)` call so the differential
|
||||
/// oracle still flips on raw payload bytes.
|
||||
pub fn emit_header_injection_harness(spec: &HarnessSpec) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
if entry_source_uses_raw_socket(&entry_source) {
|
||||
return emit_header_injection_wire_frame_harness(spec, &entry_source);
|
||||
}
|
||||
let shim = probe_shim();
|
||||
let tier_a_active = entry_source_imports_axum_header(&entry_source);
|
||||
let entry_fn = &spec.entry_name;
|
||||
let needs_percent_encoding = entry_source.contains("percent_encoding");
|
||||
|
|
@ -763,6 +776,234 @@ fn entry_source_imports_axum_header(src: &str) -> bool {
|
|||
src.contains("axum::http::HeaderMap") || src.contains("http::HeaderMap")
|
||||
}
|
||||
|
||||
/// Tier-(b) wire-frame gate for HEADER_INJECTION. Fires when the
|
||||
/// fixture binds a raw `std::net::TcpListener` and exposes the
|
||||
/// `set_cookie_value` / `create_server` / `run_once` triple the harness
|
||||
/// drives. Distinct from the axum gate because the wire-frame branch
|
||||
/// owns the response-write path itself and bypasses every framework
|
||||
/// CRLF validator.
|
||||
fn entry_source_uses_raw_socket(src: &str) -> bool {
|
||||
src.contains("TcpListener::bind") && src.contains("set_cookie_value")
|
||||
}
|
||||
|
||||
/// Tier-(b) wire-frame harness for HEADER_INJECTION (Phase 08 / Track
|
||||
/// J.6). Stages the raw-socket fixture at `src/entry.rs`, declares
|
||||
/// `mod entry;` in `main.rs`, and drives the fixture's `create_server`
|
||||
/// and `run_once` API in a worker thread while the harness opens a
|
||||
/// `TcpStream` against the bound port, issues one `GET / HTTP/1.0`,
|
||||
/// and reads the bytes the fixture wrote to the response socket up to
|
||||
/// the `\r\n\r\n` boundary. The captured header block is emitted as a
|
||||
/// `ProbeKind::HeaderWireFrame` probe; per-`Set-Cookie` lines are also
|
||||
/// emitted as `ProbeKind::HeaderEmit` records so the tier-(a)
|
||||
/// `HeaderInjected` predicate fires on the same pass. Prints a
|
||||
/// `wire_frame_len` stdout marker so e2e tests can pin the branch.
|
||||
fn emit_header_injection_wire_frame_harness(
|
||||
_spec: &HarnessSpec,
|
||||
entry_source: &str,
|
||||
) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
let needs_percent_encoding = entry_source.contains("percent_encoding");
|
||||
let mut extra_files: Vec<(String, String)> = Vec::new();
|
||||
let cargo_toml = generate_cargo_toml_with_extras(Cap::HEADER_INJECTION, needs_percent_encoding);
|
||||
extra_files.push(("Cargo.toml".into(), cargo_toml));
|
||||
|
||||
let main_rs = format!(
|
||||
r##"//! Nyx dynamic harness — HEADER_INJECTION raw-socket wire frame (Phase 08 / Track J.6).
|
||||
mod entry;
|
||||
use std::env;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{{Read, Write}};
|
||||
use std::net::TcpStream;
|
||||
use std::thread;
|
||||
use std::time::{{Duration, SystemTime, UNIX_EPOCH}};
|
||||
|
||||
{shim}
|
||||
|
||||
fn nyx_json_escape(s: &str) -> String {{
|
||||
let mut out = String::with_capacity(s.len() + 2);
|
||||
for c in s.chars() {{
|
||||
match c {{
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
c if (c as u32) < 0x20 => {{
|
||||
out.push_str(&format!("\\u{{:04x}}", c as u32));
|
||||
}}
|
||||
c => out.push(c),
|
||||
}}
|
||||
}}
|
||||
out
|
||||
}}
|
||||
|
||||
fn nyx_byte_list(bytes: &[u8]) -> String {{
|
||||
let mut out = String::with_capacity(bytes.len() * 4 + 2);
|
||||
out.push('[');
|
||||
for (i, b) in bytes.iter().enumerate() {{
|
||||
if i > 0 {{ out.push(','); }}
|
||||
out.push_str(&b.to_string());
|
||||
}}
|
||||
out.push(']');
|
||||
out
|
||||
}}
|
||||
|
||||
fn nyx_emit_record(line: &str) {{
|
||||
let p = match env::var("NYX_PROBE_PATH") {{ Ok(s) => s, Err(_) => return }};
|
||||
if p.is_empty() {{ return; }}
|
||||
if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(&p) {{
|
||||
let _ = f.write_all(line.as_bytes());
|
||||
}}
|
||||
}}
|
||||
|
||||
fn nyx_now_ns() -> u64 {{
|
||||
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos() as u64).unwrap_or(0)
|
||||
}}
|
||||
|
||||
fn nyx_header_probe(name: &str, value: &str) {{
|
||||
let now = nyx_now_ns();
|
||||
let pid = env::var("NYX_PAYLOAD_ID").unwrap_or_default();
|
||||
let mut line = String::new();
|
||||
line.push_str("{{\"sink_callee\":\"TcpStream::write_all\",\"args\":[");
|
||||
line.push_str("{{\"kind\":\"String\",\"value\":\"");
|
||||
line.push_str(&nyx_json_escape(name));
|
||||
line.push_str("\"}},{{\"kind\":\"String\",\"value\":\"");
|
||||
line.push_str(&nyx_json_escape(value));
|
||||
line.push_str("\"}}],");
|
||||
line.push_str("\"captured_at_ns\":");
|
||||
line.push_str(&now.to_string());
|
||||
line.push_str(",\"payload_id\":\"");
|
||||
line.push_str(&nyx_json_escape(&pid));
|
||||
line.push_str("\",\"kind\":{{\"kind\":\"HeaderEmit\",\"name\":\"");
|
||||
line.push_str(&nyx_json_escape(name));
|
||||
line.push_str("\",\"value\":\"");
|
||||
line.push_str(&nyx_json_escape(value));
|
||||
line.push_str("\",\"protocol\":\"wire\"}},\"witness\":{{}}}}\n");
|
||||
nyx_emit_record(&line);
|
||||
}}
|
||||
|
||||
fn nyx_wire_frame_probe(raw_bytes: &[u8]) {{
|
||||
let now = nyx_now_ns();
|
||||
let pid = env::var("NYX_PAYLOAD_ID").unwrap_or_default();
|
||||
let mut line = String::new();
|
||||
line.push_str("{{\"sink_callee\":\"TcpStream::write_all\",\"args\":[],");
|
||||
line.push_str("\"captured_at_ns\":");
|
||||
line.push_str(&now.to_string());
|
||||
line.push_str(",\"payload_id\":\"");
|
||||
line.push_str(&nyx_json_escape(&pid));
|
||||
line.push_str("\",\"kind\":{{\"kind\":\"HeaderWireFrame\",\"raw_bytes\":");
|
||||
line.push_str(&nyx_byte_list(raw_bytes));
|
||||
line.push_str("}},\"witness\":{{}}}}\n");
|
||||
nyx_emit_record(&line);
|
||||
}}
|
||||
|
||||
fn nyx_wire_frame_via_fixture(payload: &str) -> Option<Vec<u8>> {{
|
||||
// Phase 08 tier-(b): install the cookie value on the fixture, boot
|
||||
// its `TcpListener` on 127.0.0.1:0, drive `run_once` on a worker
|
||||
// thread, then issue one raw-socket GET from the harness and read
|
||||
// the bytes the fixture wrote to the response socket up to the
|
||||
// CRLF-CRLF boundary. Returns None on connect / read failure so
|
||||
// the caller can fall back to the synthetic probe.
|
||||
entry::set_cookie_value(payload.as_bytes());
|
||||
let listener = entry::create_server();
|
||||
let addr = match listener.local_addr() {{
|
||||
Ok(a) => a,
|
||||
Err(_) => return None,
|
||||
}};
|
||||
let handle = thread::spawn(move || entry::run_once(listener));
|
||||
let mut client = match TcpStream::connect_timeout(&addr, Duration::from_secs(5)) {{
|
||||
Ok(c) => c,
|
||||
Err(_) => {{
|
||||
let _ = handle.join();
|
||||
return None;
|
||||
}}
|
||||
}};
|
||||
let _ = client.set_read_timeout(Some(Duration::from_secs(2)));
|
||||
let _ = client.set_write_timeout(Some(Duration::from_secs(2)));
|
||||
if client
|
||||
.write_all(b"GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n")
|
||||
.is_err()
|
||||
{{
|
||||
let _ = handle.join();
|
||||
return None;
|
||||
}}
|
||||
let mut raw: Vec<u8> = Vec::new();
|
||||
let mut buf = [0u8; 4096];
|
||||
while raw.len() < 65536 {{
|
||||
match client.read(&mut buf) {{
|
||||
Ok(0) => break,
|
||||
Ok(n) => {{
|
||||
raw.extend_from_slice(&buf[..n]);
|
||||
if raw.windows(4).any(|w| w == b"\r\n\r\n") {{
|
||||
break;
|
||||
}}
|
||||
}}
|
||||
Err(_) => break,
|
||||
}}
|
||||
}}
|
||||
let _ = handle.join();
|
||||
let sep = raw
|
||||
.windows(4)
|
||||
.position(|w| w == b"\r\n\r\n")
|
||||
.unwrap_or(raw.len());
|
||||
Some(raw[..sep].to_vec())
|
||||
}}
|
||||
|
||||
fn main() {{
|
||||
let payload = env::var("NYX_PAYLOAD").unwrap_or_default();
|
||||
if let Some(raw_bytes) = nyx_wire_frame_via_fixture(&payload) {{
|
||||
nyx_wire_frame_probe(&raw_bytes);
|
||||
// Derive HeaderEmit records per Set-Cookie line on the wire so
|
||||
// the tier-(a) HeaderInjected predicate also fires on the same
|
||||
// harness pass. The wire-frame branch owns the bytes; the
|
||||
// HeaderEmit records are derived from them.
|
||||
for line in raw_bytes.split(|&b| b == b'\n') {{
|
||||
let trimmed: &[u8] = if line.last() == Some(&b'\r') {{
|
||||
&line[..line.len() - 1]
|
||||
}} else {{
|
||||
line
|
||||
}};
|
||||
let sep = match trimmed.iter().position(|&b| b == b':') {{
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
}};
|
||||
let name = match std::str::from_utf8(&trimmed[..sep]) {{
|
||||
Ok(s) => s,
|
||||
Err(_) => continue,
|
||||
}};
|
||||
if !name.eq_ignore_ascii_case("Set-Cookie") {{
|
||||
continue;
|
||||
}}
|
||||
let mut start = sep + 1;
|
||||
if start < trimmed.len() && trimmed[start] == b' ' {{
|
||||
start += 1;
|
||||
}}
|
||||
let value = String::from_utf8_lossy(&trimmed[start..]).into_owned();
|
||||
nyx_header_probe(name, &value);
|
||||
}}
|
||||
println!("__NYX_SINK_HIT__");
|
||||
println!("{{{{\"wire_frame_len\":{{}}}}}}", raw_bytes.len());
|
||||
return;
|
||||
}}
|
||||
// Synthetic fallback when the fixture failed to boot — keeps the
|
||||
// differential oracle live on a build/boot failure rather than
|
||||
// silently shedding the attempt.
|
||||
nyx_header_probe("Set-Cookie", &payload);
|
||||
println!("__NYX_SINK_HIT__");
|
||||
println!("{{{{\"payload_len\":{{}}}}}}", payload.len());
|
||||
}}
|
||||
"##
|
||||
);
|
||||
|
||||
HarnessSource {
|
||||
source: main_rs,
|
||||
filename: "src/main.rs".into(),
|
||||
command: vec!["target/release/nyx_harness".into()],
|
||||
extra_files,
|
||||
entry_subpath: Some("src/entry.rs".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tier-(a) gate for OPEN_REDIRECT: the fixture imports
|
||||
/// `axum::response::Redirect`.
|
||||
fn entry_source_imports_axum_redirect(src: &str) -> bool {
|
||||
|
|
@ -2354,6 +2595,113 @@ mod tests {
|
|||
assert_eq!(harness.entry_subpath.as_deref(), Some("src/entry.rs"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_injection_routes_through_wire_frame_when_raw_socket_imported() {
|
||||
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||
spec.entry_name = "run".into();
|
||||
spec.expected_cap = Cap::HEADER_INJECTION;
|
||||
spec.entry_file = "tests/dynamic_fixtures/header_injection/rust_raw/vuln.rs".into();
|
||||
let harness = emit_header_injection_harness(&spec);
|
||||
assert!(
|
||||
harness.source.contains("mod entry;"),
|
||||
"wire-frame harness must declare mod entry: {body}",
|
||||
body = harness.source,
|
||||
);
|
||||
assert!(
|
||||
!harness.source.contains("mod nyx_harness_stubs;"),
|
||||
"wire-frame harness must not pull the axum stubs: {body}",
|
||||
body = harness.source,
|
||||
);
|
||||
assert!(
|
||||
harness
|
||||
.source
|
||||
.contains("fn nyx_wire_frame_via_fixture(payload: &str)"),
|
||||
"wire-frame harness must declare the fixture-driving helper: {body}",
|
||||
body = harness.source,
|
||||
);
|
||||
assert!(
|
||||
harness.source.contains("entry::set_cookie_value(payload.as_bytes())"),
|
||||
"wire-frame harness must install cookie value on the fixture: {body}",
|
||||
body = harness.source,
|
||||
);
|
||||
assert!(
|
||||
harness.source.contains("let listener = entry::create_server();"),
|
||||
"wire-frame harness must boot the fixture's TcpListener: {body}",
|
||||
body = harness.source,
|
||||
);
|
||||
assert!(
|
||||
harness
|
||||
.source
|
||||
.contains("thread::spawn(move || entry::run_once(listener))"),
|
||||
"wire-frame harness must drive the fixture's run_once on a worker thread: {body}",
|
||||
body = harness.source,
|
||||
);
|
||||
assert!(
|
||||
harness
|
||||
.source
|
||||
.contains(".write_all(b\"GET / HTTP/1.0\\r\\nHost: 127.0.0.1\\r\\n\\r\\n\")"),
|
||||
"wire-frame harness must issue raw GET request: {body}",
|
||||
body = harness.source,
|
||||
);
|
||||
assert!(
|
||||
harness
|
||||
.source
|
||||
.contains(r#"HeaderWireFrame\",\"raw_bytes\":"#),
|
||||
"wire-frame harness must emit a HeaderWireFrame probe carrying the raw header-block bytes: {body}",
|
||||
body = harness.source,
|
||||
);
|
||||
assert!(
|
||||
harness.source.contains(r#"\"protocol\":\"wire\""#),
|
||||
"wire-frame harness must tag derived HeaderEmit records as wire protocol: {body}",
|
||||
body = harness.source,
|
||||
);
|
||||
assert!(
|
||||
harness.source.contains("wire_frame_len"),
|
||||
"wire-frame harness must emit the wire_frame_len stdout marker: {body}",
|
||||
body = harness.source,
|
||||
);
|
||||
assert_eq!(harness.entry_subpath.as_deref(), Some("src/entry.rs"));
|
||||
// Cargo.toml must still be staged so the workdir builds.
|
||||
assert!(
|
||||
harness
|
||||
.extra_files
|
||||
.iter()
|
||||
.any(|(p, _)| p == "Cargo.toml"),
|
||||
"wire-frame harness must stage Cargo.toml: {files:?}",
|
||||
files = harness
|
||||
.extra_files
|
||||
.iter()
|
||||
.map(|(p, _)| p.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_injection_wire_frame_branch_drops_when_only_axum_imported() {
|
||||
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||
spec.entry_name = "run".into();
|
||||
spec.expected_cap = Cap::HEADER_INJECTION;
|
||||
spec.entry_file = "tests/dynamic_fixtures/header_injection/rust/vuln.rs".into();
|
||||
let harness = emit_header_injection_harness(&spec);
|
||||
assert!(
|
||||
!harness
|
||||
.source
|
||||
.contains("fn nyx_wire_frame_via_fixture(payload: &str)"),
|
||||
"axum harness must not pull the wire-frame helper: {body}",
|
||||
body = harness.source,
|
||||
);
|
||||
assert!(
|
||||
!harness.source.contains("HeaderWireFrame"),
|
||||
"axum harness must not emit the HeaderWireFrame probe shape: {body}",
|
||||
body = harness.source,
|
||||
);
|
||||
assert!(
|
||||
!harness.source.contains("wire_frame_len"),
|
||||
"axum harness must not print the wire-frame stdout marker: {body}",
|
||||
body = harness.source,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_injection_tier_a_pulls_percent_encoding_when_benign_uses_it() {
|
||||
// Benign fixture imports `percent_encoding`; tier-(a) must pin
|
||||
|
|
|
|||
58
tests/dynamic_fixtures/header_injection/rust_raw/vuln.rs
Normal file
58
tests/dynamic_fixtures/header_injection/rust_raw/vuln.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Phase 08 (Track J.6) — Rust raw-socket HEADER_INJECTION vuln fixture.
|
||||
//
|
||||
// Writes the response status line and headers directly to the wire via
|
||||
// `TcpStream::write_all`, bypassing the framework-level CRLF validator
|
||||
// that axum / Tomcat would otherwise interpose. A payload carrying
|
||||
// `\r\nSet-Cookie: ...` splits the single Set-Cookie header into two on
|
||||
// the wire, producing the canonical smuggled-second-header shape that
|
||||
// `ProbeKind::HeaderWireFrame` is designed to catch.
|
||||
//
|
||||
// The harness (`src/dynamic/lang/rust.rs::emit_header_injection_harness`)
|
||||
// detects the `TcpListener::bind` token in this file and routes through
|
||||
// the tier-(b) wire-frame branch: bind a loopback `TcpListener` via
|
||||
// `create_server`, spawn the accept loop on a thread (`run_once`),
|
||||
// issue one raw `GET / HTTP/1.0\r\n` from the harness, read the bytes
|
||||
// the fixture wrote to the response socket, and emit them as a
|
||||
// `ProbeKind::HeaderWireFrame` record.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{Shutdown, TcpListener};
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Bytes go straight onto the wire with no encoding pass. The harness
|
||||
/// installs the cookie value before booting the accept loop, mirroring
|
||||
/// the JS `setCookieValue` and Python `Handler.cookie_value =` setters.
|
||||
static COOKIE_VALUE: Mutex<Vec<u8>> = Mutex::new(Vec::new());
|
||||
|
||||
pub fn set_cookie_value(value: &[u8]) {
|
||||
let mut guard = COOKIE_VALUE.lock().expect("cookie mutex poisoned");
|
||||
guard.clear();
|
||||
guard.extend_from_slice(value);
|
||||
}
|
||||
|
||||
pub fn create_server() -> TcpListener {
|
||||
TcpListener::bind("127.0.0.1:0").expect("bind ephemeral port")
|
||||
}
|
||||
|
||||
pub fn run_once(listener: TcpListener) {
|
||||
let Ok((mut socket, _addr)) = listener.accept() else {
|
||||
return;
|
||||
};
|
||||
let mut scratch = [0u8; 4096];
|
||||
let _ = socket.read(&mut scratch);
|
||||
let cookie = COOKIE_VALUE
|
||||
.lock()
|
||||
.expect("cookie mutex poisoned")
|
||||
.clone();
|
||||
let body = b"ok\n";
|
||||
let mut raw = Vec::new();
|
||||
raw.extend_from_slice(b"HTTP/1.0 200 OK\r\n");
|
||||
raw.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
|
||||
raw.extend_from_slice(b"Set-Cookie: ");
|
||||
raw.extend_from_slice(&cookie);
|
||||
raw.extend_from_slice(b"\r\n");
|
||||
raw.extend_from_slice(b"\r\n");
|
||||
raw.extend_from_slice(body);
|
||||
let _ = socket.write_all(&raw);
|
||||
let _ = socket.shutdown(Shutdown::Both);
|
||||
}
|
||||
|
|
@ -808,6 +808,95 @@ mod e2e_phase_08 {
|
|||
);
|
||||
}
|
||||
|
||||
// Phase 08 tier-(b): Rust raw-socket wire-frame fixture.
|
||||
// `tests/dynamic_fixtures/header_injection/rust_raw/vuln.rs` boots a
|
||||
// `std::net::TcpListener` via `create_server` whose `run_once`
|
||||
// handler writes raw bytes via `TcpStream::write_all`, bypassing
|
||||
// axum's `HeaderValue::from_bytes` CRLF strip. The harness boots
|
||||
// the listener on a loopback port, opens a client `TcpStream`,
|
||||
// reads the response-header block off the socket, and emits a
|
||||
// `ProbeKind::HeaderWireFrame` record. Asserts the test exercises
|
||||
// the wire-frame branch (not the synthetic fallback) by pinning
|
||||
// `wire_frame_len` in the captured stdout — that literal only
|
||||
// appears in the tier-(b) write path.
|
||||
fn build_rust_raw_spec(entry_name: &str) -> (HarnessSpec, TempDir) {
|
||||
let fixture_src = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/dynamic_fixtures/header_injection/rust_raw/vuln.rs");
|
||||
let tmp = TempDir::new().expect("create tempdir");
|
||||
let dst = tmp.path().join("vuln.rs");
|
||||
std::fs::copy(&fixture_src, &dst).expect("copy rust_raw fixture into tempdir");
|
||||
let entry_file = dst.to_string_lossy().into_owned();
|
||||
let mut digest = blake3::Hasher::new();
|
||||
digest.update(b"phase08-e2e-header-injection|rust_raw|vuln.rs");
|
||||
let spec_hash = format!("{:016x}", {
|
||||
let bytes = digest.finalize();
|
||||
u64::from_le_bytes(bytes.as_bytes()[..8].try_into().unwrap())
|
||||
});
|
||||
// Mirror the Java workdir wipe — Cargo's release build dir lives
|
||||
// under the shared workdir at `/tmp/nyx-harness/<spec_hash>`, so
|
||||
// a previous run with a different harness source can serve stale
|
||||
// cached compilation results.
|
||||
let workdir = std::path::PathBuf::from("/tmp/nyx-harness").join(&spec_hash);
|
||||
let _ = std::fs::remove_dir_all(&workdir);
|
||||
let spec = HarnessSpec {
|
||||
finding_id: spec_hash.clone(),
|
||||
entry_file: entry_file.clone(),
|
||||
entry_name: entry_name.to_owned(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: Lang::Rust,
|
||||
toolchain_id: default_toolchain_id(Lang::Rust).into(),
|
||||
payload_slot: PayloadSlot::Param(0),
|
||||
expected_cap: Cap::HEADER_INJECTION,
|
||||
constraint_hints: vec![],
|
||||
sink_file: entry_file,
|
||||
sink_line: 1,
|
||||
spec_hash: spec_hash.clone(),
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
java_toolchain: nyx_scanner::dynamic::spec::JavaToolchain::default(),
|
||||
};
|
||||
(spec, tmp)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rust_raw_socket_vuln_confirms_via_wire_frame_probe() {
|
||||
if !command_available("cargo") {
|
||||
eprintln!("SKIP rust_raw: missing cargo");
|
||||
return;
|
||||
}
|
||||
let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let (spec, _tmp) = build_rust_raw_spec("run");
|
||||
let opts = SandboxOptions {
|
||||
backend: SandboxBackend::Process,
|
||||
..SandboxOptions::default()
|
||||
};
|
||||
let outcome = match run_spec(&spec, &opts) {
|
||||
Ok(outcome) => outcome,
|
||||
Err(RunError::BuildFailed { stderr, attempts }) => {
|
||||
eprintln!(
|
||||
"SKIP rust_raw: harness build failed after {attempts} attempts: {stderr}",
|
||||
);
|
||||
return;
|
||||
}
|
||||
Err(e) => panic!("run_spec(rust_raw) errored: {e:?}"),
|
||||
};
|
||||
assert_confirmed(Lang::Rust, &outcome);
|
||||
let any_wire_frame_marker = outcome.attempts.iter().any(|a| {
|
||||
String::from_utf8_lossy(&a.outcome.stdout).contains("wire_frame_len")
|
||||
});
|
||||
assert!(
|
||||
any_wire_frame_marker,
|
||||
"rust_raw fixture must exercise the tier-(b) wire-frame harness branch; \
|
||||
expected `wire_frame_len` substring in at least one attempt's stdout, got attempts={:?}",
|
||||
outcome
|
||||
.attempts
|
||||
.iter()
|
||||
.map(|a| String::from_utf8_lossy(&a.outcome.stdout).into_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_raw_socket_vuln_confirms_via_wire_frame_probe() {
|
||||
if !command_available("python3") {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue