[pitboss] phase 09: Track J.7 + Track L.7 — OPEN_REDIRECT corpus + redirect-aware adapters

This commit is contained in:
pitboss 2026-05-18 02:32:13 -05:00
parent 5697763f28
commit b881af5d93
47 changed files with 2592 additions and 32 deletions

View file

@ -0,0 +1,54 @@
//! Go `Cap::OPEN_REDIRECT` payloads — `gin.Context.Redirect` /
//! `http.Redirect` off-origin redirect.
//!
//! Vuln payload: an absolute attacker URL spliced into
//! `c.Redirect(http.StatusFound, value)` (or
//! `http.Redirect(w, r, value, ...)`) without host validation; the
//! harness records a `ProbeKind::Redirect` probe whose `location`
//! points off-origin.
//!
//! 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.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-go-absolute",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/go/vuln.go"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
benign_control: Some(PayloadRef {
label: "open-redirect-go-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"/dashboard",
label: "open-redirect-go-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/go/benign.go"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,59 @@
//! Java `Cap::OPEN_REDIRECT` payloads —
//! `HttpServletResponse.sendRedirect` off-origin redirect.
//!
//! Vuln payload: a fully-qualified attacker URL
//! (`https://attacker.test/`). Spliced into the host's
//! `response.sendRedirect(value)` call without host validation, the
//! servlet response's `Location:` header points off-origin. The
//! harness's instrumented `sendRedirect` shim records a
//! `ProbeKind::Redirect { location: <raw>, request_host:
//! "example.com" }` probe; the predicate
//! [`crate::dynamic::oracle::ProbePredicate::RedirectHostNotIn`] sees
//! the off-allowlist host and fires.
//!
//! Benign control: same logical entry point, but the harness's benign
//! code path redirects to the relative path `/dashboard` (no host
//! component). The captured `location` has no off-origin authority
//! and the predicate stays clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-java-absolute",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/java/Vuln.java"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
benign_control: Some(PayloadRef {
label: "open-redirect-java-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"/dashboard",
label: "open-redirect-java-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/java/Benign.java"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,53 @@
//! JavaScript `Cap::OPEN_REDIRECT` payloads —
//! Express `res.redirect` off-origin redirect.
//!
//! Vuln payload: an absolute attacker URL spliced into
//! `res.redirect(value)` without host validation; the harness
//! records a `ProbeKind::Redirect` probe whose `location` points
//! off-origin.
//!
//! 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.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-js-absolute",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/js/vuln.js"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
benign_control: Some(PayloadRef {
label: "open-redirect-js-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"/dashboard",
label: "open-redirect-js-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/js/benign.js"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,26 @@
//! Open-redirect (`Cap::OPEN_REDIRECT`) per-language payload slices.
//!
//! Phase 09 (Track J.7) carves open redirects across the seven HTTP
//! framework ecosystems Nyx supports: Java
//! (`HttpServletResponse.sendRedirect`), Python (`flask.redirect`),
//! PHP (Symfony `Response::redirect` / Slim `Response::withHeader`),
//! Ruby (`Rack::Response#redirect`), JavaScript (Express
//! `res.redirect`), Go (`gin.Context.Redirect`), Rust (`axum::response::
//! Redirect::to`). Every vuln payload binds an absolute attacker URL
//! (`https://attacker.test/`) into the response writer's redirect
//! entry point; the paired benign control redirects to a same-origin
//! path (`/dashboard`). The harness's instrumented redirect shim
//! records a [`crate::dynamic::probe::ProbeKind::Redirect { location,
//! request_host }`] probe with the unmodified location and the
//! request's origin host, and the
//! [`crate::dynamic::oracle::ProbePredicate::RedirectHostNotIn`]
//! predicate fires when the captured `location` resolves off-origin
//! relative to `allowlist {request_host}`.
pub mod go;
pub mod java;
pub mod js;
pub mod php;
pub mod python;
pub mod ruby;
pub mod rust;

View file

@ -0,0 +1,55 @@
//! PHP `Cap::OPEN_REDIRECT` payloads — `Response::redirect` /
//! Symfony `RedirectResponse(...)` off-origin redirect.
//!
//! Vuln payload: an absolute attacker URL passed to
//! `header("Location: $value")` or
//! `new \Symfony\Component\HttpFoundation\RedirectResponse($value)`
//! without host validation. The harness records a
//! `ProbeKind::Redirect { location, request_host }` probe and the
//! predicate fires on the off-allowlist host.
//!
//! 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.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-php-absolute",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/php/vuln.php"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
benign_control: Some(PayloadRef {
label: "open-redirect-php-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"/dashboard",
label: "open-redirect-php-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/php/benign.php"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,54 @@
//! Python `Cap::OPEN_REDIRECT` payloads — `flask.redirect`
//! off-origin redirect.
//!
//! Vuln payload: an attacker-controlled absolute URL spliced into
//! `flask.redirect(value)` without host validation; the captured
//! `Location:` header points off-origin and the
//! [`crate::dynamic::oracle::ProbePredicate::RedirectHostNotIn`]
//! predicate fires.
//!
//! Benign control: same shape but redirects to the relative path
//! `/dashboard`, so the captured location has no authority component
//! and the predicate stays clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-python-absolute",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/python/vuln.py"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
benign_control: Some(PayloadRef {
label: "open-redirect-python-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"/dashboard",
label: "open-redirect-python-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/python/benign.py"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,53 @@
//! Ruby `Cap::OPEN_REDIRECT` payloads —
//! `Rack::Response#redirect` off-origin redirect.
//!
//! Vuln payload: an absolute attacker URL spliced into
//! `response.redirect(value)` without host validation; the harness
//! records a `ProbeKind::Redirect` probe whose `location` points
//! off-origin.
//!
//! 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.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-ruby-absolute",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/ruby/vuln.rb"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
benign_control: Some(PayloadRef {
label: "open-redirect-ruby-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"/dashboard",
label: "open-redirect-ruby-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/ruby/benign.rb"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,53 @@
//! Rust `Cap::OPEN_REDIRECT` payloads — `axum::response::Redirect::to`
//! off-origin redirect.
//!
//! Vuln payload: an absolute attacker URL spliced into
//! `Redirect::to(value)` without host validation; the harness
//! records a `ProbeKind::Redirect` probe whose `location` points
//! off-origin.
//!
//! 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.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-rust-absolute",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/rust/vuln.rs"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
benign_control: Some(PayloadRef {
label: "open-redirect-rust-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"/dashboard",
label: "open-redirect-rust-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/rust/benign.rs"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -24,8 +24,8 @@ use std::collections::HashMap;
use std::sync::OnceLock;
use super::{
cmdi, deserialize, fmt_string, header_injection, ldap, path_trav, sqli, ssrf, ssti, xpath,
xss, xxe,
cmdi, deserialize, fmt_string, header_injection, ldap, open_redirect, path_trav, sqli, ssrf,
ssti, xpath, xss, xxe,
};
use super::{CapCorpus, CuratedPayload, Oracle};
use crate::dynamic::oracle::ProbePredicate;
@ -43,7 +43,6 @@ pub const CORPUS_UNSUPPORTED_LANG_NEUTRAL: u32 = Cap::ENV_VAR.bits()
| Cap::CRYPTO.bits()
| Cap::UNAUTHORIZED_ID.bits()
| Cap::DATA_EXFIL.bits()
| Cap::OPEN_REDIRECT.bits()
| Cap::PROTOTYPE_POLLUTION.bits();
/// Flat `(Cap, Lang, slice)` table. A single cap can carry per-language
@ -83,6 +82,13 @@ const ENTRIES: &[(Cap, Lang, &[CuratedPayload])] = &[
(Cap::HEADER_INJECTION, Lang::JavaScript, header_injection::js::PAYLOADS),
(Cap::HEADER_INJECTION, Lang::Go, header_injection::go::PAYLOADS),
(Cap::HEADER_INJECTION, Lang::Rust, header_injection::rust::PAYLOADS),
(Cap::OPEN_REDIRECT, Lang::Java, open_redirect::java::PAYLOADS),
(Cap::OPEN_REDIRECT, Lang::Python, open_redirect::python::PAYLOADS),
(Cap::OPEN_REDIRECT, Lang::Php, open_redirect::php::PAYLOADS),
(Cap::OPEN_REDIRECT, Lang::Ruby, open_redirect::ruby::PAYLOADS),
(Cap::OPEN_REDIRECT, Lang::JavaScript, open_redirect::js::PAYLOADS),
(Cap::OPEN_REDIRECT, Lang::Go, open_redirect::go::PAYLOADS),
(Cap::OPEN_REDIRECT, Lang::Rust, open_redirect::rust::PAYLOADS),
];
/// Reserved for per-cap oracle defaults. Empty in Phase 02; populated by
@ -295,6 +301,7 @@ mod tests {
assert!(!payloads_for(Cap::LDAP_INJECTION).is_empty());
assert!(!payloads_for(Cap::XPATH_INJECTION).is_empty());
assert!(!payloads_for(Cap::HEADER_INJECTION).is_empty());
assert!(!payloads_for(Cap::OPEN_REDIRECT).is_empty());
}
#[test]
@ -307,7 +314,6 @@ mod tests {
Cap::CRYPTO,
Cap::UNAUTHORIZED_ID,
Cap::DATA_EXFIL,
Cap::OPEN_REDIRECT,
Cap::PROTOTYPE_POLLUTION,
];
for cap in unsupported {
@ -342,6 +348,7 @@ mod tests {
Cap::LDAP_INJECTION,
Cap::XPATH_INJECTION,
Cap::HEADER_INJECTION,
Cap::OPEN_REDIRECT,
] {
let has_vuln = payloads_for(cap).iter().any(|p| !p.is_benign);
assert!(has_vuln, "{cap:?} must have at least one vuln payload");
@ -394,6 +401,7 @@ mod tests {
Cap::LDAP_INJECTION,
Cap::XPATH_INJECTION,
Cap::HEADER_INJECTION,
Cap::OPEN_REDIRECT,
];
for cap in caps {
for p in payloads_for(cap) {
@ -421,6 +429,7 @@ mod tests {
Cap::LDAP_INJECTION,
Cap::XPATH_INJECTION,
Cap::HEADER_INJECTION,
Cap::OPEN_REDIRECT,
];
for cap in caps {
for p in payloads_for(cap) {
@ -535,6 +544,7 @@ mod tests {
Cap::LDAP_INJECTION,
Cap::XPATH_INJECTION,
Cap::HEADER_INJECTION,
Cap::OPEN_REDIRECT,
];
for cap in caps {
for p in payloads_for(cap).iter().filter(|p| p.is_benign) {