mirror of
https://github.com/elicpeter/nyx.git
synced 2026-07-03 20:41:00 +02:00
[pitboss/grind] deferred session-0027 (20260521T201327Z-3848)
This commit is contained in:
parent
0e1365455f
commit
824859008e
9 changed files with 478 additions and 2 deletions
|
|
@ -1618,9 +1618,9 @@ pub fn emit_open_redirect_harness(spec: &HarnessSpec) -> HarnessSource {
|
|||
};
|
||||
|
||||
let invoke_via_fixture = if uses_node_writer {
|
||||
"const captured = nyxRedirectViaFixture(payload);\nif (Array.isArray(captured)) {\n const [location, requestHost] = captured;\n nyxRedirectProbe(location, requestHost);\n console.log('__NYX_SINK_HIT__');\n console.log(JSON.stringify({ location: location, request_host: requestHost }));\n} else {\n // Synthetic fallback — fixture import / call failed.\n const requestHost = 'example.com';\n const location = payload;\n nyxRedirectProbe(location, requestHost);\n console.log('__NYX_SINK_HIT__');\n console.log(JSON.stringify({ location: location, request_host: requestHost }));\n}\n"
|
||||
"const captured = nyxRedirectViaFixture(payload);\nif (Array.isArray(captured)) {\n const [location, requestHost] = captured;\n nyxRedirectProbe(location, requestHost);\n nyxFollowLocation(location);\n console.log('__NYX_SINK_HIT__');\n console.log(JSON.stringify({ location: location, request_host: requestHost }));\n} else {\n // Synthetic fallback — fixture import / call failed.\n const requestHost = 'example.com';\n const location = payload;\n nyxRedirectProbe(location, requestHost);\n nyxFollowLocation(location);\n console.log('__NYX_SINK_HIT__');\n console.log(JSON.stringify({ location: location, request_host: requestHost }));\n}\n"
|
||||
} else {
|
||||
"const requestHost = 'example.com';\nconst location = payload;\nnyxRedirectProbe(location, requestHost);\nconsole.log('__NYX_SINK_HIT__');\nconsole.log(JSON.stringify({ location: location, request_host: requestHost }));\n"
|
||||
"const requestHost = 'example.com';\nconst location = payload;\nnyxRedirectProbe(location, requestHost);\nnyxFollowLocation(location);\nconsole.log('__NYX_SINK_HIT__');\nconsole.log(JSON.stringify({ location: location, request_host: requestHost }));\n"
|
||||
};
|
||||
|
||||
let body = format!(
|
||||
|
|
@ -1647,6 +1647,31 @@ function nyxRedirectProbe(location, requestHost) {{
|
|||
}}
|
||||
}}
|
||||
|
||||
// Phase 09 OOB closure: when the captured Location is a fully-qualified
|
||||
// loopback URL, follow it with a real GET so the OOB listener records
|
||||
// the per-finding nonce. Skips non-loopback hosts (no real network egress)
|
||||
// and any non-HTTP scheme. Best-effort: failures do not propagate, the
|
||||
// listener may still have observed the connect before the read errored.
|
||||
function nyxFollowLocation(location) {{
|
||||
if (!location || typeof location !== 'string') return;
|
||||
const lower = location.toLowerCase();
|
||||
if (!(lower.startsWith('http://127.0.0.1')
|
||||
|| lower.startsWith('http://localhost')
|
||||
|| lower.startsWith('http://host-gateway'))) {{
|
||||
return;
|
||||
}}
|
||||
try {{
|
||||
const http = require('http');
|
||||
const req = http.get(location, {{ timeout: 2000 }}, (res) => {{
|
||||
res.resume();
|
||||
}});
|
||||
req.on('error', () => {{}});
|
||||
req.on('timeout', () => {{ try {{ req.destroy(); }} catch (e) {{}} }});
|
||||
}} catch (e) {{
|
||||
// best-effort OOB fetch
|
||||
}}
|
||||
}}
|
||||
|
||||
{via_fixture}const payload = process.env.NYX_PAYLOAD || '';
|
||||
{invoke_via_fixture}"#
|
||||
);
|
||||
|
|
@ -3118,4 +3143,72 @@ mod tests {
|
|||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_open_redirect_harness_ships_follow_location_helper() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase09_js_test_follow_location");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let entry = dir.join("vuln.js");
|
||||
std::fs::write(
|
||||
&entry,
|
||||
"const express = require('express');\nfunction run(req, res, value) { res.redirect(value); }\nmodule.exports = { run };\n",
|
||||
)
|
||||
.unwrap();
|
||||
let h = emit_open_redirect_harness(&make_redirect_spec(
|
||||
entry.to_str().unwrap(),
|
||||
"run",
|
||||
));
|
||||
assert!(
|
||||
h.source.contains("function nyxFollowLocation(location)"),
|
||||
"OPEN_REDIRECT harness must declare the nyxFollowLocation helper: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("http.get(location"),
|
||||
"follow-location helper must call http.get on the captured URL: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("lower.startsWith('http://127.0.0.1')")
|
||||
&& h.source.contains("lower.startsWith('http://localhost')")
|
||||
&& h.source.contains("lower.startsWith('http://host-gateway')"),
|
||||
"follow-location helper must gate on loopback host prefixes: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("nyxRedirectProbe(location, requestHost);\n nyxFollowLocation(location);"),
|
||||
"tier-(a) must follow the captured Location after emitting the probe: {}",
|
||||
h.source
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_open_redirect_harness_follows_synthetic_location_in_fallback() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase09_js_test_follow_fallback");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let entry = dir.join("vuln.js");
|
||||
std::fs::write(
|
||||
&entry,
|
||||
"function run(req, res, value) { res.redirect(value); }\nmodule.exports = { run };\n",
|
||||
)
|
||||
.unwrap();
|
||||
let h = emit_open_redirect_harness(&make_redirect_spec(
|
||||
entry.to_str().unwrap(),
|
||||
"run",
|
||||
));
|
||||
assert!(
|
||||
h.source.contains("function nyxFollowLocation(location)"),
|
||||
"fallback path must still declare nyxFollowLocation: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("nyxRedirectProbe(location, requestHost);\nnyxFollowLocation(location);"),
|
||||
"fallback path must follow the synthetic location after the probe: {}",
|
||||
h.source
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1350,6 +1350,7 @@ pub fn emit_open_redirect_harness(spec: &HarnessSpec) -> HarnessSource {
|
|||
if ($captured !== null) {{
|
||||
[$location, $requestHost] = $captured;
|
||||
_nyx_redirect_probe($location, $requestHost);
|
||||
_nyx_follow_location($location);
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
echo json_encode(['location' => $location, 'request_host' => $requestHost]) . "\n";
|
||||
return;
|
||||
|
|
@ -1384,6 +1385,25 @@ function _nyx_redirect_probe(string $location, string $requestHost): void {{
|
|||
@file_put_contents($p, json_encode($rec) . "\n", FILE_APPEND);
|
||||
}}
|
||||
|
||||
// Phase 09 OOB closure: when the captured Location is a fully-qualified
|
||||
// loopback URL, follow it with a real GET so the OOB listener records
|
||||
// the per-finding nonce. Skips non-loopback hosts (no real network egress)
|
||||
// and any non-HTTP scheme. Best-effort: failures do not propagate, the
|
||||
// listener may still have observed the connect before the read errored.
|
||||
function _nyx_follow_location(string $location): void {{
|
||||
if ($location === '') return;
|
||||
$lower = strtolower($location);
|
||||
if (!(str_starts_with($lower, 'http://127.0.0.1')
|
||||
|| str_starts_with($lower, 'http://localhost')
|
||||
|| str_starts_with($lower, 'http://host-gateway'))) {{
|
||||
return;
|
||||
}}
|
||||
$ctx = stream_context_create([
|
||||
'http' => ['timeout' => 2, 'follow_location' => 0, 'ignore_errors' => true],
|
||||
]);
|
||||
@file_get_contents($location, false, $ctx);
|
||||
}}
|
||||
|
||||
{via_fixture}function _nyx_run(): void {{
|
||||
$payload = (string) (getenv('NYX_PAYLOAD') ?: '');
|
||||
{invoke_via_fixture}// Synthetic fallback — records the raw payload as the redirect
|
||||
|
|
@ -1394,6 +1414,7 @@ function _nyx_redirect_probe(string $location, string $requestHost): void {{
|
|||
$requestHost = 'example.com';
|
||||
$location = $payload;
|
||||
_nyx_redirect_probe($location, $requestHost);
|
||||
_nyx_follow_location($location);
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
echo json_encode(['location' => $location, 'request_host' => $requestHost]) . "\n";
|
||||
}}
|
||||
|
|
@ -2416,4 +2437,49 @@ mod tests {
|
|||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_open_redirect_harness_ships_follow_location_helper() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase09_php_test_follow_location");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let entry = dir.join("vuln.php");
|
||||
std::fs::write(
|
||||
&entry,
|
||||
"<?php\nuse Symfony\\Component\\HttpFoundation\\RedirectResponse;\nfunction run($v) { return new RedirectResponse($v); }\n",
|
||||
)
|
||||
.unwrap();
|
||||
let h = emit_open_redirect_harness(&make_redirect_spec(
|
||||
entry.to_str().unwrap(),
|
||||
"run",
|
||||
));
|
||||
assert!(
|
||||
h.source.contains("function _nyx_follow_location(string $location): void"),
|
||||
"OPEN_REDIRECT harness must declare the _nyx_follow_location helper: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("file_get_contents($location, false, $ctx)"),
|
||||
"follow-location helper must call file_get_contents with a stream context: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("'timeout' => 2"),
|
||||
"follow-location helper must pin the stream context timeout to 2 seconds: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("str_starts_with($lower, 'http://127.0.0.1')")
|
||||
&& h.source.contains("str_starts_with($lower, 'http://localhost')")
|
||||
&& h.source.contains("str_starts_with($lower, 'http://host-gateway')"),
|
||||
"follow-location helper must gate on loopback host prefixes: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("_nyx_redirect_probe($location, $requestHost);\n _nyx_follow_location($location);"),
|
||||
"tier-(a) must follow the captured Location after emitting the probe: {}",
|
||||
h.source
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2090,6 +2090,7 @@ pub fn emit_open_redirect_harness(spec: &HarnessSpec) -> HarnessSource {
|
|||
if captured is not None:
|
||||
location, request_host = captured
|
||||
_nyx_redirect_probe(location, request_host)
|
||||
_nyx_follow_location(location)
|
||||
print("__NYX_SINK_HIT__", flush=True)
|
||||
sys.stdout.write(json.dumps({"location": location, "request_host": request_host}) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
|
@ -2106,6 +2107,7 @@ pub fn emit_open_redirect_harness(spec: &HarnessSpec) -> HarnessSource {
|
|||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
{probe}
|
||||
|
||||
|
|
@ -2128,11 +2130,35 @@ def _nyx_redirect_probe(location, request_host):
|
|||
__nyx_emit(rec)
|
||||
|
||||
|
||||
# Phase 09 OOB closure: when the captured Location is a fully-qualified
|
||||
# loopback URL, follow it with a real GET so the OOB listener records
|
||||
# the per-finding nonce. Skips non-loopback hosts (no real network egress)
|
||||
# and any non-HTTP scheme. Best-effort: failures do not propagate, the
|
||||
# listener may still have observed the connect before the read errored.
|
||||
def _nyx_follow_location(location):
|
||||
if not location:
|
||||
return
|
||||
lower = location.lower()
|
||||
if not (
|
||||
lower.startswith("http://127.0.0.1")
|
||||
or lower.startswith("http://localhost")
|
||||
or lower.startswith("http://host-gateway")
|
||||
):
|
||||
return
|
||||
try:
|
||||
with urllib.request.urlopen(location, timeout=2.0) as resp:
|
||||
resp.read(1)
|
||||
except Exception:
|
||||
# best-effort OOB fetch
|
||||
pass
|
||||
|
||||
|
||||
{via_fixture}def _nyx_run():
|
||||
payload = os.environ.get("NYX_PAYLOAD", "")
|
||||
{invoke_via_fixture} request_host = "example.com"
|
||||
location = payload
|
||||
_nyx_redirect_probe(location, request_host)
|
||||
_nyx_follow_location(location)
|
||||
print("__NYX_SINK_HIT__", flush=True)
|
||||
sys.stdout.write(json.dumps({{"location": location, "request_host": request_host}}) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
|
@ -3397,4 +3423,72 @@ mod tests {
|
|||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_open_redirect_harness_ships_follow_location_helper() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase09_py_test_follow_location");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let entry = dir.join("vuln.py");
|
||||
std::fs::write(
|
||||
&entry,
|
||||
"from flask import redirect\ndef run(value):\n return redirect(value)\n",
|
||||
)
|
||||
.unwrap();
|
||||
let h = emit_open_redirect_harness(&make_redirect_spec(
|
||||
entry.to_str().unwrap(),
|
||||
"run",
|
||||
));
|
||||
assert!(
|
||||
h.source.contains("def _nyx_follow_location(location):"),
|
||||
"OPEN_REDIRECT harness must declare the _nyx_follow_location helper: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("import urllib.request"),
|
||||
"OPEN_REDIRECT harness must import urllib.request for the loopback follow: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("urllib.request.urlopen(location, timeout=2.0)"),
|
||||
"follow-location helper must call urllib.request.urlopen with a 2-second timeout: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("startswith(\"http://127.0.0.1\")")
|
||||
&& h.source.contains("startswith(\"http://localhost\")")
|
||||
&& h.source.contains("startswith(\"http://host-gateway\")"),
|
||||
"follow-location helper must gate on loopback host prefixes: {}",
|
||||
h.source
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_open_redirect_harness_follows_captured_location_in_tier_a() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase09_py_test_follow_captured");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let entry = dir.join("vuln.py");
|
||||
std::fs::write(
|
||||
&entry,
|
||||
"from flask import redirect\ndef run(value):\n return redirect(value)\n",
|
||||
)
|
||||
.unwrap();
|
||||
let h = emit_open_redirect_harness(&make_redirect_spec(
|
||||
entry.to_str().unwrap(),
|
||||
"run",
|
||||
));
|
||||
assert!(
|
||||
h.source.contains("_nyx_redirect_probe(location, request_host)\n _nyx_follow_location(location)"),
|
||||
"tier-(a) must follow the captured Location after emitting the probe: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("_nyx_redirect_probe(location, request_host)\n _nyx_follow_location(location)"),
|
||||
"tier-(b) fallback must also follow the synthetic location after the probe: {}",
|
||||
h.source
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1319,6 +1319,7 @@ end
|
|||
if captured
|
||||
location, request_host = captured
|
||||
_nyx_redirect_probe(location, request_host)
|
||||
_nyx_follow_location(location)
|
||||
STDOUT.puts '__NYX_SINK_HIT__'
|
||||
STDOUT.puts JSON.generate({ 'location' => location, 'request_host' => request_host })
|
||||
STDOUT.flush
|
||||
|
|
@ -1331,6 +1332,8 @@ end
|
|||
let body = format!(
|
||||
r#"# Nyx dynamic harness — OPEN_REDIRECT Rack::Response#redirect (Phase 09 / Track J.7).
|
||||
require 'json'
|
||||
require 'net/http'
|
||||
require 'uri'
|
||||
|
||||
{shim}
|
||||
|
||||
|
|
@ -1350,11 +1353,36 @@ def _nyx_redirect_probe(location, request_host)
|
|||
File.open(p, 'a') {{ |f| f.write(rec.to_json + "\n") }}
|
||||
end
|
||||
|
||||
# Phase 09 OOB closure: when the captured Location is a fully-qualified
|
||||
# loopback URL, follow it with a real GET so the OOB listener records
|
||||
# the per-finding nonce. Skips non-loopback hosts (no real network egress)
|
||||
# and any non-HTTP scheme. Best-effort: failures do not propagate, the
|
||||
# listener may still have observed the connect before the read errored.
|
||||
def _nyx_follow_location(location)
|
||||
return if location.nil? || location.empty?
|
||||
lower = location.to_s.downcase
|
||||
unless lower.start_with?('http://127.0.0.1') ||
|
||||
lower.start_with?('http://localhost') ||
|
||||
lower.start_with?('http://host-gateway')
|
||||
return
|
||||
end
|
||||
begin
|
||||
uri = URI(location)
|
||||
Net::HTTP.start(uri.host, uri.port, open_timeout: 2, read_timeout: 2) do |http|
|
||||
req = Net::HTTP::Get.new(uri.request_uri)
|
||||
http.request(req) {{ |resp| resp.read_body {{ |_chunk| break }} }}
|
||||
end
|
||||
rescue StandardError
|
||||
# best-effort OOB fetch
|
||||
end
|
||||
end
|
||||
|
||||
{via_fixture}def _nyx_run
|
||||
payload = ENV['NYX_PAYLOAD'] || ''
|
||||
{invoke_via_fixture} request_host = 'example.com'
|
||||
location = payload
|
||||
_nyx_redirect_probe(location, request_host)
|
||||
_nyx_follow_location(location)
|
||||
STDOUT.puts '__NYX_SINK_HIT__'
|
||||
STDOUT.puts JSON.generate({{ 'location' => location, 'request_host' => request_host }})
|
||||
STDOUT.flush
|
||||
|
|
@ -2067,4 +2095,50 @@ mod tests {
|
|||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_open_redirect_harness_ships_follow_location_helper() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase09_rb_test_follow_location");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let entry = dir.join("vuln.rb");
|
||||
std::fs::write(
|
||||
&entry,
|
||||
"require 'rack'\n\
|
||||
def run(value)\n r = Rack::Response.new\n r.redirect(value)\n r\nend\n",
|
||||
)
|
||||
.unwrap();
|
||||
let h = emit_open_redirect_harness(&make_redirect_spec(
|
||||
entry.to_str().unwrap(),
|
||||
"run",
|
||||
));
|
||||
assert!(
|
||||
h.source.contains("def _nyx_follow_location(location)"),
|
||||
"OPEN_REDIRECT harness must declare the _nyx_follow_location helper: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("require 'net/http'") && h.source.contains("require 'uri'"),
|
||||
"OPEN_REDIRECT harness must require net/http and uri for the loopback follow: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("Net::HTTP.start(uri.host, uri.port"),
|
||||
"follow-location helper must invoke Net::HTTP.start: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("start_with?('http://127.0.0.1')")
|
||||
&& h.source.contains("start_with?('http://localhost')")
|
||||
&& h.source.contains("start_with?('http://host-gateway')"),
|
||||
"follow-location helper must gate on loopback host prefixes: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("_nyx_redirect_probe(location, request_host)\n _nyx_follow_location(location)"),
|
||||
"tier-(a) must follow the captured Location after emitting the probe: {}",
|
||||
h.source
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue