mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-21 20:18:06 +02:00
[pitboss/grind] deferred session-0030 (20260521T201327Z-3848)
This commit is contained in:
parent
824859008e
commit
3d8f988453
6 changed files with 306 additions and 11 deletions
|
|
@ -972,7 +972,7 @@ pub fn emit_open_redirect_harness(spec: &HarnessSpec) -> HarnessSource {
|
|||
"##
|
||||
));
|
||||
via_fixture_invoke.push_str(
|
||||
"\tif loc, ok := nyxRedirectViaFixture(payload); ok {\n\t\tnyxRedirectProbe(loc, requestHost)\n\t} else {\n\t\tnyxRedirectProbe(payload, requestHost)\n\t}\n",
|
||||
"\tif loc, ok := nyxRedirectViaFixture(payload); ok {\n\t\tnyxRedirectProbe(loc, requestHost)\n\t\tnyxFollowLocation(loc)\n\t} else {\n\t\tnyxRedirectProbe(payload, requestHost)\n\t\tnyxFollowLocation(payload)\n\t}\n",
|
||||
);
|
||||
} else if imports_net_http {
|
||||
// Plain stdlib `http.Redirect(w, r, value, status)` fixture.
|
||||
|
|
@ -999,10 +999,15 @@ pub fn emit_open_redirect_harness(spec: &HarnessSpec) -> HarnessSource {
|
|||
"##
|
||||
));
|
||||
via_fixture_invoke.push_str(
|
||||
"\tif loc, ok := nyxRedirectViaFixture(payload); ok {\n\t\tnyxRedirectProbe(loc, requestHost)\n\t} else {\n\t\tnyxRedirectProbe(payload, requestHost)\n\t}\n",
|
||||
"\tif loc, ok := nyxRedirectViaFixture(payload); ok {\n\t\tnyxRedirectProbe(loc, requestHost)\n\t\tnyxFollowLocation(loc)\n\t} else {\n\t\tnyxRedirectProbe(payload, requestHost)\n\t\tnyxFollowLocation(payload)\n\t}\n",
|
||||
);
|
||||
} else {
|
||||
via_fixture_invoke.push_str("\tnyxRedirectProbe(payload, requestHost)\n");
|
||||
// Tier-(b) fallback gate doesn't import net/http, but the OOB
|
||||
// follower itself needs it. Pull the stdlib net/http surface
|
||||
// unconditionally so `nyxFollowLocation` compiles.
|
||||
extra_imports.push_str("\t\"net/http\"\n");
|
||||
via_fixture_invoke
|
||||
.push_str("\tnyxRedirectProbe(payload, requestHost)\n\tnyxFollowLocation(payload)\n");
|
||||
}
|
||||
|
||||
let source = format!(
|
||||
|
|
@ -1034,6 +1039,30 @@ func nyxRedirectProbe(location, requestHost string) {{
|
|||
}})
|
||||
}}
|
||||
|
||||
// Phase 09 OOB closure: when the captured Location is a loopback URL,
|
||||
// follow it with a real GET so the OOB listener observes the per-finding
|
||||
// nonce. Skips non-loopback hosts and non-HTTP schemes (no real network
|
||||
// egress). Best-effort: errors do not propagate; the listener may still
|
||||
// record the TCP connect before the read fails.
|
||||
func nyxFollowLocation(location string) {{
|
||||
if location == "" {{
|
||||
return
|
||||
}}
|
||||
if !(strings.HasPrefix(location, "http://127.0.0.1") ||
|
||||
strings.HasPrefix(location, "http://localhost") ||
|
||||
strings.HasPrefix(location, "http://host-gateway")) {{
|
||||
return
|
||||
}}
|
||||
client := &http.Client{{Timeout: 2 * time.Second}}
|
||||
resp, err := client.Get(location)
|
||||
if err != nil {{
|
||||
return
|
||||
}}
|
||||
defer resp.Body.Close()
|
||||
buf := make([]byte, 1)
|
||||
_, _ = resp.Body.Read(buf)
|
||||
}}
|
||||
|
||||
{via_fixture_decl}func main() {{
|
||||
__nyx_install_crash_guard("gin.Context.Redirect")
|
||||
defer __nyx_recover_crash("gin.Context.Redirect")()
|
||||
|
|
@ -2225,6 +2254,70 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_open_redirect_harness_ships_follow_location_helper() {
|
||||
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||
spec.entry_name = "Run".into();
|
||||
spec.expected_cap = Cap::OPEN_REDIRECT;
|
||||
spec.entry_file = "/nonexistent/missing.go".into();
|
||||
let harness = emit_open_redirect_harness(&spec);
|
||||
assert!(
|
||||
harness.source.contains("func nyxFollowLocation(location string)"),
|
||||
"OPEN_REDIRECT harness must declare the nyxFollowLocation helper",
|
||||
);
|
||||
assert!(
|
||||
harness.source.contains("strings.HasPrefix(location, \"http://127.0.0.1\")"),
|
||||
"follower must gate on loopback 127.0.0.1 host prefix",
|
||||
);
|
||||
assert!(
|
||||
harness.source.contains("strings.HasPrefix(location, \"http://localhost\")"),
|
||||
"follower must gate on loopback localhost host prefix",
|
||||
);
|
||||
assert!(
|
||||
harness.source.contains("strings.HasPrefix(location, \"http://host-gateway\")"),
|
||||
"follower must gate on loopback host-gateway prefix",
|
||||
);
|
||||
assert!(
|
||||
harness.source.contains("client.Get(location)"),
|
||||
"follower must drive a real http.Client.Get against the captured Location",
|
||||
);
|
||||
// Tier-(b) callsite must call the follower on the synthetic payload.
|
||||
assert!(
|
||||
harness
|
||||
.source
|
||||
.contains("nyxRedirectProbe(payload, requestHost)\n\tnyxFollowLocation(payload)"),
|
||||
"tier-(b) callsite must invoke nyxFollowLocation after the synthetic probe",
|
||||
);
|
||||
// Even tier-(b) must pull in net/http so the follower compiles.
|
||||
assert!(
|
||||
harness.source.contains("\"net/http\""),
|
||||
"OPEN_REDIRECT harness must always import net/http so nyxFollowLocation compiles",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_open_redirect_harness_follows_captured_location_in_tier_a() {
|
||||
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||
spec.entry_name = "Run".into();
|
||||
spec.expected_cap = Cap::OPEN_REDIRECT;
|
||||
spec.entry_file = "tests/dynamic_fixtures/open_redirect/go/vuln.go".into();
|
||||
let harness = emit_open_redirect_harness(&spec);
|
||||
// Tier-(a) gin: when fixture call succeeds, follow the captured loc.
|
||||
assert!(
|
||||
harness
|
||||
.source
|
||||
.contains("nyxRedirectProbe(loc, requestHost)\n\t\tnyxFollowLocation(loc)"),
|
||||
"tier-(a) callsite must invoke nyxFollowLocation on the captured Location",
|
||||
);
|
||||
// Tier-(a) fixture-call-failed branch falls back to payload-as-loc.
|
||||
assert!(
|
||||
harness
|
||||
.source
|
||||
.contains("nyxRedirectProbe(payload, requestHost)\n\t\tnyxFollowLocation(payload)"),
|
||||
"tier-(a) fixture-failure branch must still follow the synthetic payload",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gin_stub_pkg_exposes_redirect_method() {
|
||||
let stub = gin_stub_pkg();
|
||||
|
|
|
|||
|
|
@ -923,9 +923,9 @@ pub fn emit_open_redirect_harness(spec: &HarnessSpec) -> HarnessSource {
|
|||
|
||||
"##
|
||||
);
|
||||
via_fixture_invoke = " let location = match nyx_redirect_via_fixture(payload.clone()) {\n Some(loc) if !loc.is_empty() => loc,\n _ => payload.clone(),\n };\n nyx_redirect_probe(&location, request_host);\n".to_owned();
|
||||
via_fixture_invoke = " let location = match nyx_redirect_via_fixture(payload.clone()) {\n Some(loc) if !loc.is_empty() => loc,\n _ => payload.clone(),\n };\n nyx_redirect_probe(&location, request_host);\n nyx_follow_location(&location);\n".to_owned();
|
||||
} else {
|
||||
via_fixture_invoke = " let location = payload.clone();\n nyx_redirect_probe(&location, request_host);\n".to_owned();
|
||||
via_fixture_invoke = " let location = payload.clone();\n nyx_redirect_probe(&location, request_host);\n nyx_follow_location(&location);\n".to_owned();
|
||||
}
|
||||
|
||||
let cargo_toml = generate_cargo_toml(Cap::OPEN_REDIRECT);
|
||||
|
|
@ -982,6 +982,52 @@ fn nyx_redirect_probe(location: &str, request_host: &str) {{
|
|||
}}
|
||||
}}
|
||||
|
||||
// Phase 09 OOB closure: when the captured Location is a loopback URL,
|
||||
// follow it with a zero-dep `TcpStream` GET so the OOB listener
|
||||
// observes the per-finding nonce. Skips non-loopback hosts and
|
||||
// non-HTTP schemes (no real network egress). Best-effort: errors do
|
||||
// not propagate; the listener may still record the TCP connect before
|
||||
// the read fails.
|
||||
fn nyx_follow_location(location: &str) {{
|
||||
if location.is_empty() {{ return; }}
|
||||
let loopback = location.starts_with("http://127.0.0.1")
|
||||
|| location.starts_with("http://localhost")
|
||||
|| location.starts_with("http://host-gateway");
|
||||
if !loopback {{ return; }}
|
||||
let rest = match location.strip_prefix("http://") {{
|
||||
Some(r) => r,
|
||||
None => return,
|
||||
}};
|
||||
let (authority, path) = match rest.find('/') {{
|
||||
Some(i) => (&rest[..i], &rest[i..]),
|
||||
None => (rest, "/"),
|
||||
}};
|
||||
let (host, port): (&str, u16) = match authority.rfind(':') {{
|
||||
Some(i) => {{
|
||||
let p = authority[i + 1..].parse::<u16>().unwrap_or(80);
|
||||
(&authority[..i], p)
|
||||
}}
|
||||
None => (authority, 80),
|
||||
}};
|
||||
use std::io::{{Read, Write}};
|
||||
use std::net::{{TcpStream, ToSocketAddrs}};
|
||||
use std::time::Duration;
|
||||
let addr = match (host, port).to_socket_addrs() {{
|
||||
Ok(mut it) => match it.next() {{ Some(a) => a, None => return }},
|
||||
Err(_) => return,
|
||||
}};
|
||||
let mut stream = match TcpStream::connect_timeout(&addr, Duration::from_secs(2)) {{
|
||||
Ok(s) => s,
|
||||
Err(_) => return,
|
||||
}};
|
||||
let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
|
||||
let _ = stream.set_write_timeout(Some(Duration::from_secs(2)));
|
||||
let req = format!("GET {{path}} HTTP/1.0\r\nHost: {{host}}\r\nConnection: close\r\n\r\n", path = path, host = host);
|
||||
let _ = stream.write_all(req.as_bytes());
|
||||
let mut buf = [0u8; 1];
|
||||
let _ = stream.read(&mut buf);
|
||||
}}
|
||||
|
||||
{via_fixture_decl}fn main() {{
|
||||
let payload = env::var("NYX_PAYLOAD").unwrap_or_default();
|
||||
let request_host = "example.com";
|
||||
|
|
@ -2252,6 +2298,60 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_open_redirect_harness_ships_follow_location_helper() {
|
||||
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||
spec.entry_name = "run".into();
|
||||
spec.expected_cap = Cap::OPEN_REDIRECT;
|
||||
spec.entry_file = "/nonexistent/missing.rs".into();
|
||||
let harness = emit_open_redirect_harness(&spec);
|
||||
assert!(
|
||||
harness.source.contains("fn nyx_follow_location(location: &str)"),
|
||||
"OPEN_REDIRECT harness must declare the nyx_follow_location helper",
|
||||
);
|
||||
for prefix in [
|
||||
"http://127.0.0.1",
|
||||
"http://localhost",
|
||||
"http://host-gateway",
|
||||
] {
|
||||
assert!(
|
||||
harness
|
||||
.source
|
||||
.contains(&format!("starts_with(\"{prefix}\")")),
|
||||
"follower must gate on loopback {prefix} prefix",
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
harness.source.contains("TcpStream::connect_timeout"),
|
||||
"follower must drive a zero-dep TcpStream::connect_timeout against the captured Location",
|
||||
);
|
||||
assert!(
|
||||
harness.source.contains("GET {path} HTTP/1.0"),
|
||||
"follower must write a HTTP/1.0 GET request line",
|
||||
);
|
||||
// Tier-(b) callsite must call the follower on the synthetic payload.
|
||||
assert!(
|
||||
harness
|
||||
.source
|
||||
.contains("nyx_redirect_probe(&location, request_host);\n nyx_follow_location(&location);"),
|
||||
"tier-(b) callsite must invoke nyx_follow_location after the synthetic probe",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_open_redirect_harness_follows_captured_location_in_tier_a() {
|
||||
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||
spec.entry_name = "run".into();
|
||||
spec.expected_cap = Cap::OPEN_REDIRECT;
|
||||
spec.entry_file = "tests/dynamic_fixtures/open_redirect/rust/vuln.rs".into();
|
||||
let harness = emit_open_redirect_harness(&spec);
|
||||
// Tier-(a) callsite: captured loc → probe + follow.
|
||||
assert!(
|
||||
harness.source.contains("nyx_redirect_probe(&location, request_host);\n nyx_follow_location(&location);"),
|
||||
"tier-(a) callsite must invoke nyx_follow_location on the captured Location",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cargo_toml_extras_pins_percent_encoding_when_requested() {
|
||||
let cargo = generate_cargo_toml_with_extras(Cap::HEADER_INJECTION, true);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue