mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0015 (20260521T201327Z-3848)
This commit is contained in:
parent
c95dcc00fb
commit
3b62269a04
2 changed files with 539 additions and 11 deletions
|
|
@ -1221,10 +1221,20 @@ console.log(JSON.stringify({{ render: rendered }}));
|
|||
/// staged document, and writes a `ProbeKind::Xpath { nodes_returned }`
|
||||
/// probe whose `n` is the count returned. Mirrors the synthetic-
|
||||
/// harness pattern used by Phase 03 / 04 / 05 / 06.
|
||||
pub fn emit_xpath_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||
pub fn emit_xpath_harness(spec: &HarnessSpec) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
let corpus_filename = crate::dynamic::stubs::xpath_document::XPATH_CORPUS_FILENAME;
|
||||
let corpus_xml = crate::dynamic::stubs::xpath_document::XPATH_CORPUS_XML;
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let entry_stem = derive_js_entry_stem(&spec.entry_file);
|
||||
let entry_name = if spec.entry_name.is_empty() {
|
||||
"run".to_owned()
|
||||
} else {
|
||||
spec.entry_name.clone()
|
||||
};
|
||||
let uses_real_xpath = entry_source.contains("require('xpath')")
|
||||
|| entry_source.contains("require(\"xpath\")");
|
||||
|
||||
let body = format!(
|
||||
r#"// Nyx dynamic harness — XPATH_INJECTION xpath.select (Phase 07 / Track J.5).
|
||||
{shim}
|
||||
|
|
@ -1263,6 +1273,34 @@ function nyxXpathSelect(expr) {{
|
|||
return NYX_XPATH_USERS.length;
|
||||
}}
|
||||
|
||||
function nyxXpathViaFixture(payload) {{
|
||||
// Phase 07 tier-(a): require the fixture and call its
|
||||
// `{entry_name}` so the real `xpath.select` (or other XPath evaluator
|
||||
// the fixture chooses) runs against the staged corpus document.
|
||||
// Returns the node count, or `null` when the require / lookup / call
|
||||
// fails (e.g. the `xpath` npm package is not installed on the host)
|
||||
// so the caller can fall back to the inline matcher.
|
||||
let _entry;
|
||||
try {{
|
||||
_entry = require('./{entry_stem}');
|
||||
}} catch (e) {{
|
||||
return null;
|
||||
}}
|
||||
const fn = _entry && (typeof _entry === 'function' ? _entry : _entry['{entry_name}']);
|
||||
if (typeof fn !== 'function') return null;
|
||||
let result;
|
||||
try {{
|
||||
result = fn(payload);
|
||||
}} catch (e) {{
|
||||
// Malformed XPath / parse error / etc. — treat as 0-node return
|
||||
// so a benign fixture that rejects the payload stays NotConfirmed.
|
||||
return 0;
|
||||
}}
|
||||
if (result == null) return 0;
|
||||
if (typeof result.length === 'number') return result.length;
|
||||
return 0;
|
||||
}}
|
||||
|
||||
function nyxXpathProbe(expr, nodesReturned) {{
|
||||
const p = process.env.NYX_PROBE_PATH;
|
||||
if (!p) return;
|
||||
|
|
@ -1283,13 +1321,23 @@ function nyxXpathProbe(expr, nodesReturned) {{
|
|||
|
||||
const payload = process.env.NYX_PAYLOAD || '';
|
||||
const expr = "//user[@name='" + payload + "']";
|
||||
const nodes = nyxXpathSelect(expr);
|
||||
let nodes = nyxXpathViaFixture(payload);
|
||||
if (nodes === null) {{
|
||||
nodes = nyxXpathSelect(expr);
|
||||
}}
|
||||
nyxXpathProbe(expr, nodes);
|
||||
console.log('__NYX_SINK_HIT__');
|
||||
console.log(JSON.stringify({{ expr: expr, nodes_returned: nodes }}));
|
||||
"#
|
||||
);
|
||||
let extra_files = vec![(corpus_filename.to_owned(), corpus_xml.to_owned())];
|
||||
let mut extra_files = vec![(corpus_filename.to_owned(), corpus_xml.to_owned())];
|
||||
if uses_real_xpath {
|
||||
extra_files.push(("package.json".to_owned(), package_json_xpath()));
|
||||
extra_files.push((
|
||||
"package-lock.json".to_owned(),
|
||||
package_lock_skeleton("nyx-harness-xpath"),
|
||||
));
|
||||
}
|
||||
HarnessSource {
|
||||
source: body,
|
||||
filename: "harness.js".to_owned(),
|
||||
|
|
@ -1299,6 +1347,25 @@ console.log(JSON.stringify({{ expr: expr, nodes_returned: nodes }}));
|
|||
}
|
||||
}
|
||||
|
||||
/// Map an entry file path like `tests/.../vuln.js` to the basename
|
||||
/// (without extension) the harness will `require('./<stem>')`. Falls
|
||||
/// back to `"vuln"` so the harness still attempts a require when the
|
||||
/// path is unusable (the inline matcher fires when the require fails).
|
||||
fn derive_js_entry_stem(entry_file: &str) -> String {
|
||||
PathBuf::from(entry_file)
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|s| s.to_owned())
|
||||
.unwrap_or_else(|| "vuln".to_owned())
|
||||
}
|
||||
|
||||
/// `package.json` bundling `xpath` + `@xmldom/xmldom` so the JS XPath
|
||||
/// fixtures can `require('xpath')` / `require('@xmldom/xmldom')` from
|
||||
/// the harness workdir. Mirrors `package_json_for` but pins two deps.
|
||||
fn package_json_xpath() -> String {
|
||||
"{\n \"name\": \"nyx-harness-xpath\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"dependencies\": {\n \"xpath\": \"^0.0.34\",\n \"@xmldom/xmldom\": \"^0.8.10\"\n }\n}\n".to_owned()
|
||||
}
|
||||
|
||||
/// Phase 08 — Track J.6 header-injection harness for Node
|
||||
/// (`http.ServerResponse#setHeader`).
|
||||
///
|
||||
|
|
@ -2561,4 +2628,117 @@ mod tests {
|
|||
"stub recorder must read NYX_HTTP_LOG"
|
||||
);
|
||||
}
|
||||
|
||||
fn make_xpath_spec(entry_file: &str, entry_name: &str) -> HarnessSpec {
|
||||
let mut spec = make_spec(EntryKind::Function, entry_name, PayloadSlot::Param(0));
|
||||
spec.expected_cap = Cap::XPATH_INJECTION;
|
||||
spec.entry_file = entry_file.into();
|
||||
spec.entry_name = entry_name.into();
|
||||
spec
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_xpath_harness_drives_fixture_through_real_xpath_when_imported() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase07_js_test_drive_fixture");
|
||||
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 xpath = require('xpath');\n\
|
||||
function run(name) { return []; }\n\
|
||||
module.exports = { run };\n",
|
||||
)
|
||||
.unwrap();
|
||||
let h = emit_xpath_harness(&make_xpath_spec(entry.to_str().unwrap(), "run"));
|
||||
assert!(
|
||||
h.source.contains("function nyxXpathViaFixture(payload)"),
|
||||
"tier-(a) harness must define nyxXpathViaFixture: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("require('./vuln')"),
|
||||
"tier-(a) harness must require the staged fixture: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("_entry['run']"),
|
||||
"tier-(a) harness must look up the named entry function: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("if (typeof result.length === 'number') return result.length;"),
|
||||
"tier-(a) harness must count nodes via the returned array's .length: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("nodes = nyxXpathSelect(expr);"),
|
||||
"tier-(a) harness must preserve the inline matcher as a fallback: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.extra_files
|
||||
.iter()
|
||||
.any(|(p, c)| p == "package.json" && c.contains("\"xpath\"")),
|
||||
"tier-(a) harness must stage a package.json with the xpath dep",
|
||||
);
|
||||
assert!(
|
||||
h.extra_files
|
||||
.iter()
|
||||
.any(|(p, c)| p == "package.json" && c.contains("@xmldom/xmldom")),
|
||||
"tier-(a) harness must stage a package.json with the xmldom dep",
|
||||
);
|
||||
assert!(
|
||||
h.extra_files
|
||||
.iter()
|
||||
.any(|(p, _)| p == "package-lock.json"),
|
||||
"tier-(a) harness must stage a package-lock.json",
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_xpath_harness_falls_back_to_inline_matcher_without_xpath_require() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase07_js_test_no_xpath_require");
|
||||
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(name) { return []; }\nmodule.exports = { run };\n",
|
||||
)
|
||||
.unwrap();
|
||||
let h = emit_xpath_harness(&make_xpath_spec(entry.to_str().unwrap(), "run"));
|
||||
assert!(
|
||||
!h.extra_files.iter().any(|(p, _)| p == "package.json"),
|
||||
"fallback path must not stage a package.json (xpath dep would be unused)",
|
||||
);
|
||||
assert!(
|
||||
!h.extra_files
|
||||
.iter()
|
||||
.any(|(p, _)| p == "package-lock.json"),
|
||||
"fallback path must not stage a package-lock.json",
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_xpath_harness_derives_entry_stem_from_entry_file() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase07_js_test_stem_derive");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let entry = dir.join("benign.js");
|
||||
std::fs::write(
|
||||
&entry,
|
||||
"const xpath = require('xpath');\nfunction run(name) { return []; }\nmodule.exports = { run };\n",
|
||||
)
|
||||
.unwrap();
|
||||
let h = emit_xpath_harness(&make_xpath_spec(entry.to_str().unwrap(), "run"));
|
||||
assert!(
|
||||
h.source.contains("require('./benign')"),
|
||||
"harness must require the staged fixture by its file_stem: {}",
|
||||
h.source
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1889,12 +1889,93 @@ fn derive_module_name(entry_file: &str) -> String {
|
|||
/// pre-encoded via `urllib.parse.quote`, so the captured value
|
||||
/// carries `%0D%0A` (not the raw bytes) and the predicate stays
|
||||
/// clear.
|
||||
pub fn emit_header_injection_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||
pub fn emit_header_injection_harness(spec: &HarnessSpec) -> HarnessSource {
|
||||
let probe = probe_shim();
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let module_name = derive_module_name(&spec.entry_file);
|
||||
let entry_name = if spec.entry_name.is_empty() {
|
||||
"run".to_owned()
|
||||
} else {
|
||||
spec.entry_name.clone()
|
||||
};
|
||||
let uses_flask = entry_source.contains("from flask")
|
||||
|| entry_source.contains("import flask")
|
||||
|| entry_source.contains("werkzeug.wrappers");
|
||||
let via_fixture = if uses_flask {
|
||||
format!(
|
||||
r#"def _nyx_header_via_fixture(payload):
|
||||
# Phase 08 tier-(a): import the fixture, monkey-patch
|
||||
# `werkzeug.datastructures.Headers.__setitem__` to capture every
|
||||
# name/value pair the fixture writes *before* werkzeug's strict
|
||||
# CRLF validator runs. This mirrors the Java permissive servlet
|
||||
# stub at `src/dynamic/lang/java_servlet_stubs.rs::http_servlet_response`,
|
||||
# so a vuln payload with raw `\r\n` is recorded verbatim and a
|
||||
# benign control whose bytes are URL-encoded is recorded
|
||||
# URL-encoded. Returns `None` when werkzeug is missing or the
|
||||
# fixture cannot be imported so the caller can fall back to the
|
||||
# inline synthetic probe.
|
||||
try:
|
||||
import werkzeug.datastructures as _wzd
|
||||
except Exception:
|
||||
return None
|
||||
captured = []
|
||||
_orig_setitem = _wzd.Headers.__setitem__
|
||||
def _nyx_setitem(self, key, value):
|
||||
try:
|
||||
captured.append((str(key), str(value)))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
_orig_setitem(self, key, value)
|
||||
except Exception:
|
||||
# werkzeug>=2.x rejects CRLF in header values. Swallow
|
||||
# the validator's exception so the captor still records
|
||||
# the would-have-been-written bytes.
|
||||
pass
|
||||
_wzd.Headers.__setitem__ = _nyx_setitem
|
||||
sys.path.insert(0, ".")
|
||||
try:
|
||||
try:
|
||||
mod = importlib.import_module("{module_name}")
|
||||
except Exception:
|
||||
return None
|
||||
fn = getattr(mod, "{entry_name}", None)
|
||||
if fn is None:
|
||||
return None
|
||||
try:
|
||||
fn(payload)
|
||||
except Exception:
|
||||
# Fixture itself raised (validator path, missing dep, etc.)
|
||||
# — return whatever the captor recorded before the throw.
|
||||
pass
|
||||
return captured
|
||||
finally:
|
||||
_wzd.Headers.__setitem__ = _orig_setitem
|
||||
|
||||
|
||||
"#
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let invoke_via_fixture = if uses_flask {
|
||||
r#" captured = _nyx_header_via_fixture(payload)
|
||||
if captured:
|
||||
for name, value in captured:
|
||||
_nyx_header_probe(name, value)
|
||||
print("__NYX_SINK_HIT__", flush=True)
|
||||
sys.stdout.write(json.dumps({"headers": [list(p) for p in captured]}) + "\n")
|
||||
sys.stdout.flush()
|
||||
return
|
||||
"#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let importlib_import = if uses_flask { "import importlib\n" } else { "" };
|
||||
let body = format!(
|
||||
r#"#!/usr/bin/env python3
|
||||
"""Nyx dynamic harness — HEADER_INJECTION flask.Response.headers.__setitem__ (Phase 08 / Track J.6)."""
|
||||
import json
|
||||
{importlib_import}import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
|
@ -1917,9 +1998,9 @@ def _nyx_header_probe(name, value):
|
|||
__nyx_emit(rec)
|
||||
|
||||
|
||||
def _nyx_run():
|
||||
{via_fixture}def _nyx_run():
|
||||
payload = os.environ.get("NYX_PAYLOAD", "")
|
||||
# Synthetic instrumented setter — mirrors
|
||||
{invoke_via_fixture} # Synthetic fallback — mirrors
|
||||
# `werkzeug.datastructures.Headers.__setitem__` semantics: the
|
||||
# value bytes flow through unmodified, so a tainted payload that
|
||||
# carries raw `\r\n` lands on the wire as a header split.
|
||||
|
|
@ -1954,12 +2035,74 @@ if __name__ == "__main__":
|
|||
/// [`crate::dynamic::oracle::ProbePredicate::RedirectHostNotIn`]
|
||||
/// oracle; the paired benign control redirects to a same-origin
|
||||
/// path and leaves the predicate clear.
|
||||
pub fn emit_open_redirect_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||
pub fn emit_open_redirect_harness(spec: &HarnessSpec) -> HarnessSource {
|
||||
let probe = probe_shim();
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let module_name = derive_module_name(&spec.entry_file);
|
||||
let entry_name = if spec.entry_name.is_empty() {
|
||||
"run".to_owned()
|
||||
} else {
|
||||
spec.entry_name.clone()
|
||||
};
|
||||
let uses_flask =
|
||||
entry_source.contains("from flask") || entry_source.contains("import flask");
|
||||
let via_fixture = if uses_flask {
|
||||
format!(
|
||||
r#"def _nyx_redirect_via_fixture(payload):
|
||||
# Phase 09 tier-(a): import the fixture, call its `{entry_name}` so
|
||||
# the real `flask.redirect` runs, then read the bound `Location:`
|
||||
# header off the returned response. Returns `(location, request_host)`
|
||||
# on success, or `None` when the import / call fails so the caller
|
||||
# can fall back to the inline synthetic probe.
|
||||
sys.path.insert(0, ".")
|
||||
try:
|
||||
mod = importlib.import_module("{module_name}")
|
||||
except Exception:
|
||||
return None
|
||||
fn = getattr(mod, "{entry_name}", None)
|
||||
if fn is None:
|
||||
return None
|
||||
try:
|
||||
response = fn(payload)
|
||||
except Exception:
|
||||
# Fixture raised (validator path, missing dep, etc.) — drop
|
||||
# tier-(a) and let the caller fall back.
|
||||
return None
|
||||
try:
|
||||
location = response.headers.get("Location", "")
|
||||
except Exception:
|
||||
return None
|
||||
if not isinstance(location, str):
|
||||
try:
|
||||
location = str(location)
|
||||
except Exception:
|
||||
return None
|
||||
return (location, "example.com")
|
||||
|
||||
|
||||
"#
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let invoke_via_fixture = if uses_flask {
|
||||
r#" captured = _nyx_redirect_via_fixture(payload)
|
||||
if captured is not None:
|
||||
location, request_host = captured
|
||||
_nyx_redirect_probe(location, request_host)
|
||||
print("__NYX_SINK_HIT__", flush=True)
|
||||
sys.stdout.write(json.dumps({"location": location, "request_host": request_host}) + "\n")
|
||||
sys.stdout.flush()
|
||||
return
|
||||
"#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let importlib_import = if uses_flask { "import importlib\n" } else { "" };
|
||||
let body = format!(
|
||||
r#"#!/usr/bin/env python3
|
||||
"""Nyx dynamic harness — OPEN_REDIRECT flask.redirect (Phase 09 / Track J.7)."""
|
||||
import json
|
||||
{importlib_import}import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
|
@ -1985,9 +2128,9 @@ def _nyx_redirect_probe(location, request_host):
|
|||
__nyx_emit(rec)
|
||||
|
||||
|
||||
def _nyx_run():
|
||||
{via_fixture}def _nyx_run():
|
||||
payload = os.environ.get("NYX_PAYLOAD", "")
|
||||
request_host = "example.com"
|
||||
{invoke_via_fixture} request_host = "example.com"
|
||||
location = payload
|
||||
_nyx_redirect_probe(location, request_host)
|
||||
print("__NYX_SINK_HIT__", flush=True)
|
||||
|
|
@ -3049,4 +3192,209 @@ mod tests {
|
|||
"module name must come from the entry-file stem, not a hard-coded literal",
|
||||
);
|
||||
}
|
||||
|
||||
fn make_header_spec(entry_file: &str, entry_name: &str) -> HarnessSpec {
|
||||
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||
spec.expected_cap = Cap::HEADER_INJECTION;
|
||||
spec.entry_file = entry_file.to_owned();
|
||||
spec.entry_name = entry_name.to_owned();
|
||||
spec
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_header_injection_harness_routes_through_fixture_when_flask_imported() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase08_py_test_drive_fixture");
|
||||
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 Response\n\
|
||||
def run(value):\n response = Response('ok')\n response.headers['Set-Cookie'] = value\n return response\n",
|
||||
)
|
||||
.unwrap();
|
||||
let h = emit_header_injection_harness(&make_header_spec(
|
||||
entry.to_str().unwrap(),
|
||||
"run",
|
||||
));
|
||||
assert!(
|
||||
h.source.contains("def _nyx_header_via_fixture(payload):"),
|
||||
"tier-(a) harness must define the fixture-routing helper: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("import werkzeug.datastructures"),
|
||||
"tier-(a) harness must monkey-patch werkzeug Headers: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("_wzd.Headers.__setitem__ = _nyx_setitem"),
|
||||
"tier-(a) harness must install the permissive captor: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("importlib.import_module(\"vuln\")"),
|
||||
"tier-(a) harness must import the fixture by its file stem: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("getattr(mod, \"run\", None)"),
|
||||
"tier-(a) harness must look up the named entry function: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("captured = _nyx_header_via_fixture(payload)"),
|
||||
"harness main must call the fixture-routing helper first: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source
|
||||
.contains("_nyx_header_probe(\"Set-Cookie\", payload)")
|
||||
|| h.source.contains("value = payload\n _nyx_header_probe(name, value)"),
|
||||
"fallback path must still emit a synthetic probe: {}",
|
||||
h.source
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_header_injection_harness_falls_back_when_flask_not_imported() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase08_py_test_no_flask");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let entry = dir.join("vuln.py");
|
||||
std::fs::write(
|
||||
&entry,
|
||||
"def run(value):\n return value\n",
|
||||
)
|
||||
.unwrap();
|
||||
let h = emit_header_injection_harness(&make_header_spec(
|
||||
entry.to_str().unwrap(),
|
||||
"run",
|
||||
));
|
||||
assert!(
|
||||
!h.source.contains("import werkzeug.datastructures"),
|
||||
"fallback path must not import werkzeug: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
!h.source.contains("def _nyx_header_via_fixture"),
|
||||
"fallback path must not define the fixture-routing helper: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("value = payload\n _nyx_header_probe(name, value)"),
|
||||
"fallback path must keep the synthetic probe: {}",
|
||||
h.source
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_header_injection_harness_derives_module_name_from_entry_file() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase08_py_test_module_derive");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let entry = dir.join("benign.py");
|
||||
std::fs::write(
|
||||
&entry,
|
||||
"from flask import Response\n\
|
||||
def run(v):\n return Response('ok')\n",
|
||||
)
|
||||
.unwrap();
|
||||
let h = emit_header_injection_harness(&make_header_spec(
|
||||
entry.to_str().unwrap(),
|
||||
"run",
|
||||
));
|
||||
assert!(
|
||||
h.source.contains("importlib.import_module(\"benign\")"),
|
||||
"module name must come from the entry-file stem: {}",
|
||||
h.source
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
fn make_redirect_spec(entry_file: &str, entry_name: &str) -> HarnessSpec {
|
||||
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||
spec.expected_cap = Cap::OPEN_REDIRECT;
|
||||
spec.entry_file = entry_file.to_owned();
|
||||
spec.entry_name = entry_name.to_owned();
|
||||
spec
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_open_redirect_harness_routes_through_fixture_when_flask_imported() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase09_py_test_drive_fixture");
|
||||
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_redirect_via_fixture(payload):"),
|
||||
"tier-(a) harness must define the fixture-routing helper: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("importlib.import_module(\"vuln\")"),
|
||||
"tier-(a) harness must import the fixture by its file stem: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("response.headers.get(\"Location\", \"\")"),
|
||||
"tier-(a) harness must read the Location header off the returned response: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("captured = _nyx_redirect_via_fixture(payload)"),
|
||||
"harness main must call the fixture-routing helper first: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("location = payload\n _nyx_redirect_probe(location, request_host)"),
|
||||
"fallback path must keep the synthetic probe: {}",
|
||||
h.source
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_open_redirect_harness_falls_back_when_flask_not_imported() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase09_py_test_no_flask");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let entry = dir.join("vuln.py");
|
||||
std::fs::write(
|
||||
&entry,
|
||||
"def run(value):\n return value\n",
|
||||
)
|
||||
.unwrap();
|
||||
let h = emit_open_redirect_harness(&make_redirect_spec(
|
||||
entry.to_str().unwrap(),
|
||||
"run",
|
||||
));
|
||||
assert!(
|
||||
!h.source.contains("def _nyx_redirect_via_fixture"),
|
||||
"fallback path must not define the fixture-routing helper: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
!h.source.contains("importlib.import_module"),
|
||||
"fallback path must not import the fixture: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("location = payload\n _nyx_redirect_probe(location, request_host)"),
|
||||
"fallback path must keep the synthetic probe: {}",
|
||||
h.source
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue