[pitboss/grind] deferred session-0027 (20260521T201327Z-3848)

This commit is contained in:
pitboss 2026-05-21 23:00:09 -05:00
parent 0e1365455f
commit 824859008e
9 changed files with 478 additions and 2 deletions

View file

@ -9,6 +9,14 @@
//! Benign control: same shape but redirects to the same-origin path
//! `/dashboard`, so the captured `location` has no authority
//! component and the predicate stays clear.
//!
//! OOB-nonce variant (added 2026-05-22): when the runner attaches an
//! [`crate::dynamic::oob::OobListener`] the harness follows the
//! captured `Location:` URL via a real `http.get` against the loopback
//! nonce URL so the listener records the per-finding callback. Ordered
//! first so the runner exercises the OOB observation path before the
//! absolute-URL vuln below triggers and short-circuits iteration; runs
//! without a listener skip cleanly (runner `oob_nonce_slot` branch).
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
@ -16,6 +24,26 @@ use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
// `bytes` is unused when `oob_nonce_slot = true`; the runner
// substitutes a per-finding loopback URL (see runner.rs:405-413).
bytes: b"",
label: "open-redirect-js-oob-nonce",
oracle: Oracle::OobCallback { host: "127.0.0.1" },
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/js/vuln.js"],
oob_nonce_slot: true,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: Some(
"OOB-nonce open-redirect payload self-confirms via the per-finding listener \
callback when the harness follows the captured Location URL with http.get; \
no benign URL can hit the nonce path.",
),
},
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-js-absolute",

View file

@ -11,6 +11,16 @@
//! Benign control: same shape but redirects to the same-origin path
//! `/dashboard`, so the captured `location` has no authority
//! component and the predicate stays clear.
//!
//! OOB-nonce variant (added 2026-05-22): when the runner attaches an
//! [`crate::dynamic::oob::OobListener`] the harness follows the
//! captured `Location:` URL via a real
//! `file_get_contents($location, stream_context_create(...))` against
//! the loopback nonce URL so the listener records the per-finding
//! callback. Ordered first so the runner exercises the OOB observation
//! path before the absolute-URL vuln below triggers and short-circuits
//! iteration; runs without a listener skip cleanly (runner
//! `oob_nonce_slot` branch).
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
@ -18,6 +28,27 @@ use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
// `bytes` is unused when `oob_nonce_slot = true`; the runner
// substitutes a per-finding loopback URL (see runner.rs:405-413).
bytes: b"",
label: "open-redirect-php-oob-nonce",
oracle: Oracle::OobCallback { host: "127.0.0.1" },
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/php/vuln.php"],
oob_nonce_slot: true,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: Some(
"OOB-nonce open-redirect payload self-confirms via the per-finding listener \
callback when the harness follows the captured Location URL with \
file_get_contents under a stream context timeout; no benign URL can hit \
the nonce path.",
),
},
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-php-absolute",

View file

@ -10,6 +10,15 @@
//! Benign control: same shape but redirects to the relative path
//! `/dashboard`, so the captured location has no authority component
//! and the predicate stays clear.
//!
//! OOB-nonce variant (added 2026-05-22): when the runner attaches an
//! [`crate::dynamic::oob::OobListener`] the harness follows the
//! captured `Location:` URL via a real `urllib.request.urlopen`
//! against the loopback nonce URL so the listener records the per-finding
//! callback. Ordered first so the runner exercises the OOB observation
//! path before the absolute-URL vuln below triggers and short-circuits
//! iteration; runs without a listener skip cleanly (runner
//! `oob_nonce_slot` branch).
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
@ -17,6 +26,26 @@ use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
// `bytes` is unused when `oob_nonce_slot = true`; the runner
// substitutes a per-finding loopback URL (see runner.rs:405-413).
bytes: b"",
label: "open-redirect-python-oob-nonce",
oracle: Oracle::OobCallback { host: "127.0.0.1" },
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/python/vuln.py"],
oob_nonce_slot: true,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: Some(
"OOB-nonce open-redirect payload self-confirms via the per-finding listener \
callback when the harness follows the captured Location URL with \
urllib.request.urlopen; no benign URL can hit the nonce path.",
),
},
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-python-absolute",

View file

@ -9,6 +9,15 @@
//! Benign control: same shape but redirects to the same-origin path
//! `/dashboard`, so the captured `location` has no authority
//! component and the predicate stays clear.
//!
//! OOB-nonce variant (added 2026-05-22): when the runner attaches an
//! [`crate::dynamic::oob::OobListener`] the harness follows the
//! captured `Location:` URL via a real `Net::HTTP.get_response` against
//! the loopback nonce URL so the listener records the per-finding
//! callback. Ordered first so the runner exercises the OOB observation
//! path before the absolute-URL vuln below triggers and short-circuits
//! iteration; runs without a listener skip cleanly (runner
//! `oob_nonce_slot` branch).
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
@ -16,6 +25,26 @@ use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
// `bytes` is unused when `oob_nonce_slot = true`; the runner
// substitutes a per-finding loopback URL (see runner.rs:405-413).
bytes: b"",
label: "open-redirect-ruby-oob-nonce",
oracle: Oracle::OobCallback { host: "127.0.0.1" },
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/ruby/vuln.rb"],
oob_nonce_slot: true,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: Some(
"OOB-nonce open-redirect payload self-confirms via the per-finding listener \
callback when the harness follows the captured Location URL with \
Net::HTTP.get_response; no benign URL can hit the nonce path.",
),
},
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-ruby-absolute",

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -711,4 +711,36 @@ mod e2e_phase_09 {
};
assert_oob_recorded(&outcome, "open-redirect-java-oob-nonce");
}
#[test]
fn python_open_redirect_oob_loopback_records_callback() {
let Some(outcome) = run_oob(Lang::Python, "vuln.py", "run") else {
return;
};
assert_oob_recorded(&outcome, "open-redirect-python-oob-nonce");
}
#[test]
fn js_open_redirect_oob_loopback_records_callback() {
let Some(outcome) = run_oob(Lang::JavaScript, "vuln.js", "run") else {
return;
};
assert_oob_recorded(&outcome, "open-redirect-js-oob-nonce");
}
#[test]
fn ruby_open_redirect_oob_loopback_records_callback() {
let Some(outcome) = run_oob(Lang::Ruby, "vuln.rb", "run") else {
return;
};
assert_oob_recorded(&outcome, "open-redirect-ruby-oob-nonce");
}
#[test]
fn php_open_redirect_oob_loopback_records_callback() {
let Some(outcome) = run_oob(Lang::Php, "vuln.php", "run") else {
return;
};
assert_oob_recorded(&outcome, "open-redirect-php-oob-nonce");
}
}