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

This commit is contained in:
pitboss 2026-05-21 19:51:35 -05:00
parent e0104a4785
commit b4677bf6ca

View file

@ -1369,13 +1369,105 @@ fn package_json_xpath() -> String {
/// Phase 08 — Track J.6 header-injection harness for Node
/// (`http.ServerResponse#setHeader`).
///
/// Reads `NYX_PAYLOAD`, calls a synthetic instrumented
/// `res.setHeader('Set-Cookie', value)` shim that records the
/// *unmodified* value bytes (including any embedded `\r\n`) via a
/// `ProbeKind::HeaderEmit` probe. Mirrors the synthetic-harness
/// pattern used by Phase 03 / 04 / 05 / 06 / 07.
pub fn emit_header_injection_harness(_spec: &HarnessSpec) -> HarnessSource {
/// Reads `NYX_PAYLOAD` and, when the fixture imports `http` or
/// `express`, routes through tier-(a): `require('./<entry-stem>')` +
/// look up the named entry function + call it with a permissive `res`
/// mock whose `setHeader` records every `(name, value)` pair the
/// fixture writes verbatim *before* Node's CRLF validator would
/// reject the call. Mirrors the Python werkzeug-Headers monkey-patch
/// at `src/dynamic/lang/python.rs::emit_header_injection_harness` and
/// the Java permissive servlet stub at
/// `src/dynamic/lang/java_servlet_stubs.rs::http_servlet_response`.
/// Falls back to the inline `nyxHeaderProbe('Set-Cookie', payload)`
/// synthetic probe when the fixture does not import a Node response
/// writer or when the tier-(a) call fails (require throws, entry
/// function missing, etc.).
pub fn emit_header_injection_harness(spec: &HarnessSpec) -> HarnessSource {
let shim = probe_shim();
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_node_writer = entry_source.contains("require('http')")
|| entry_source.contains("require(\"http\")")
|| entry_source.contains("require('express')")
|| entry_source.contains("require(\"express\")")
|| entry_source.contains("from 'http'")
|| entry_source.contains("from \"http\"")
|| entry_source.contains("from 'express'")
|| entry_source.contains("from \"express\"");
let via_fixture = if uses_node_writer {
format!(
r#"function nyxHeaderViaFixture(payload) {{
// Phase 08 tier-(a): require the fixture, call its `{entry_name}`
// with a permissive `res` mock whose `setHeader` records every
// `(name, value)` pair verbatim *before* Node's CRLF validator
// would reject the call. Returns the captured pairs as an array
// of `[name, value]` tuples, or `null` when the require / lookup /
// call fails so the caller can fall back to the inline probe.
const captured = [];
const res = {{
setHeader(name, value) {{
try {{
captured.push([String(name), String(value)]);
}} catch (e) {{
// ignore — captor is best-effort
}}
}},
getHeader(_name) {{ return undefined; }},
removeHeader(_name) {{}},
writeHead(_status, headers) {{
if (headers && typeof headers === 'object') {{
for (const k of Object.keys(headers)) {{
try {{ captured.push([String(k), String(headers[k])]); }} catch (e) {{}}
}}
}}
}},
end() {{}},
statusCode: 200,
}};
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;
// Phase 08 fixtures use `run(res, value)`; the Express open-redirect
// shape is `run(req, res, value)`. Try the two-arg path first, then
// fall through to the three-arg path so an Express handler that
// calls `res.setHeader` also lands.
try {{
fn(res, payload);
}} catch (e) {{
captured.length = 0;
try {{
fn({{ headers: {{}}, method: 'GET', url: '/' }}, res, payload);
}} catch (e2) {{
// both signatures threw — return whatever was captured before
// the throw, or null when nothing landed
}}
}}
return captured;
}}
"#
)
} else {
String::new()
};
let invoke_via_fixture = if uses_node_writer {
"const captured = nyxHeaderViaFixture(payload);\nif (Array.isArray(captured) && captured.length > 0) {\n for (const [hname, hvalue] of captured) {\n nyxHeaderProbe(hname, hvalue);\n }\n console.log('__NYX_SINK_HIT__');\n console.log(JSON.stringify({ headers: captured.map(([n, v]) => [n, v]) }));\n} else {\n // Synthetic fallback — fixture import / call failed.\n const name = 'Set-Cookie';\n const value = payload;\n nyxHeaderProbe(name, value);\n console.log('__NYX_SINK_HIT__');\n console.log(JSON.stringify({ name: name, value: value }));\n}\n"
} else {
"const name = 'Set-Cookie';\nconst value = payload;\nnyxHeaderProbe(name, value);\nconsole.log('__NYX_SINK_HIT__');\nconsole.log(JSON.stringify({ name: name, value: value }));\n"
};
let body = format!(
r#"// Nyx dynamic harness — HEADER_INJECTION http.ServerResponse#setHeader (Phase 08 / Track J.6).
{shim}
@ -1401,13 +1493,8 @@ function nyxHeaderProbe(name, value) {{
}}
}}
const payload = process.env.NYX_PAYLOAD || '';
const name = 'Set-Cookie';
const value = payload;
nyxHeaderProbe(name, value);
console.log('__NYX_SINK_HIT__');
console.log(JSON.stringify({{ name: name, value: value }}));
"#
{via_fixture}const payload = process.env.NYX_PAYLOAD || '';
{invoke_via_fixture}"#
);
HarnessSource {
source: body,
@ -1421,12 +1508,121 @@ console.log(JSON.stringify({{ name: name, value: value }}));
/// Phase 09 — Track J.7 open-redirect harness for Node (Express
/// `res.redirect`).
///
/// Reads `NYX_PAYLOAD`, calls a synthetic instrumented
/// `res.redirect(value)` shim that records the bound `Location:`
/// value plus the request's origin host via a `ProbeKind::Redirect`
/// probe.
pub fn emit_open_redirect_harness(_spec: &HarnessSpec) -> HarnessSource {
/// Reads `NYX_PAYLOAD` and, when the fixture imports `express` or
/// `http`, routes through tier-(a): `require('./<entry-stem>')` +
/// look up the named entry function + call it with a permissive `res`
/// mock whose `redirect` / `setHeader('Location', …)` record the
/// bound URL. Mirrors the Python tier-(a) at
/// `src/dynamic/lang/python.rs::emit_open_redirect_harness`: call the
/// fixture, read the Location header off the response. Falls back to
/// the inline synthetic probe (`nyxRedirectProbe(payload, …)`) when
/// the fixture does not import a Node response writer or when the
/// tier-(a) call fails.
pub fn emit_open_redirect_harness(spec: &HarnessSpec) -> HarnessSource {
let shim = probe_shim();
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_node_writer = entry_source.contains("require('http')")
|| entry_source.contains("require(\"http\")")
|| entry_source.contains("require('express')")
|| entry_source.contains("require(\"express\")")
|| entry_source.contains("from 'http'")
|| entry_source.contains("from \"http\"")
|| entry_source.contains("from 'express'")
|| entry_source.contains("from \"express\"");
let via_fixture = if uses_node_writer {
format!(
r#"function nyxRedirectViaFixture(payload) {{
// Phase 09 tier-(a): require the fixture, call its `{entry_name}`
// with a permissive `res` mock whose `redirect` / `setHeader` both
// record the bound `Location:` URL. Returns `[location, host]` or
// `null` when the require / lookup / call fails so the caller can
// fall back to the inline synthetic probe.
let location = null;
const recordLocation = (value) => {{
if (location === null && value !== undefined && value !== null) {{
location = String(value);
}}
}};
const res = {{
redirect(...args) {{
// Express signatures: redirect(url) | redirect(status, url).
if (args.length === 1) {{
recordLocation(args[0]);
}} else if (args.length >= 2) {{
recordLocation(args[1]);
}}
}},
setHeader(name, value) {{
if (String(name).toLowerCase() === 'location') {{
recordLocation(value);
}}
}},
set(name, value) {{
if (String(name).toLowerCase() === 'location') {{
recordLocation(value);
}}
}},
location(value) {{
recordLocation(value);
}},
writeHead(_status, headers) {{
if (headers && typeof headers === 'object') {{
for (const k of Object.keys(headers)) {{
if (k.toLowerCase() === 'location') {{
recordLocation(headers[k]);
}}
}}
}}
}},
end() {{}},
statusCode: 200,
}};
const req = {{ headers: {{}}, method: 'GET', url: '/' }};
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;
// Phase 09 fixtures use `run(req, res, value)` (Express handler
// signature). Try the three-arg path first; if it throws try the
// two-arg shape `(res, value)` so a fixture without an explicit
// `req` parameter still lands.
try {{
fn(req, res, payload);
}} catch (e) {{
try {{
fn(res, payload);
}} catch (e2) {{
// both signatures threw — return whatever was captured before
// the throw, or null when nothing landed
}}
}}
if (location === null) return null;
return [location, 'example.com'];
}}
"#
)
} else {
String::new()
};
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"
} 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"
};
let body = format!(
r#"// Nyx dynamic harness — OPEN_REDIRECT res.redirect (Phase 09 / Track J.7).
{shim}
@ -1451,13 +1647,8 @@ function nyxRedirectProbe(location, requestHost) {{
}}
}}
const payload = process.env.NYX_PAYLOAD || '';
const requestHost = 'example.com';
const location = payload;
nyxRedirectProbe(location, requestHost);
console.log('__NYX_SINK_HIT__');
console.log(JSON.stringify({{ location: location, request_host: requestHost }}));
"#
{via_fixture}const payload = process.env.NYX_PAYLOAD || '';
{invoke_via_fixture}"#
);
HarnessSource {
source: body,
@ -2741,4 +2932,190 @@ mod tests {
);
let _ = std::fs::remove_dir_all(&dir);
}
fn make_header_spec(entry_file: &str, entry_name: &str) -> HarnessSpec {
let mut spec = make_spec(EntryKind::Function, entry_name, PayloadSlot::Param(0));
spec.expected_cap = Cap::HEADER_INJECTION;
spec.entry_file = entry_file.into();
spec.entry_name = entry_name.into();
spec
}
fn make_redirect_spec(entry_file: &str, entry_name: &str) -> HarnessSpec {
let mut spec = make_spec(EntryKind::Function, entry_name, PayloadSlot::Param(0));
spec.expected_cap = Cap::OPEN_REDIRECT;
spec.entry_file = entry_file.into();
spec.entry_name = entry_name.into();
spec
}
#[test]
fn emit_header_injection_harness_routes_through_fixture_when_http_required() {
let dir = std::env::temp_dir().join("nyx_phase08_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 http = require('http');\nfunction run(res, value) { res.setHeader('Set-Cookie', value); }\nmodule.exports = { run };\n",
)
.unwrap();
let h = emit_header_injection_harness(&make_header_spec(
entry.to_str().unwrap(),
"run",
));
assert!(
h.source.contains("function nyxHeaderViaFixture(payload)"),
"tier-(a) harness must define nyxHeaderViaFixture: {}",
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("captured.push([String(name), String(value)])"),
"tier-(a) harness must record (name, value) pairs verbatim: {}",
h.source
);
assert!(
h.source.contains("Synthetic fallback"),
"tier-(a) harness must preserve the inline probe as a fallback: {}",
h.source
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn emit_header_injection_harness_falls_back_when_http_not_required() {
let dir = std::env::temp_dir().join("nyx_phase08_js_test_no_http");
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(res, value) { res.setHeader('Set-Cookie', value); }\nmodule.exports = { run };\n",
)
.unwrap();
let h = emit_header_injection_harness(&make_header_spec(
entry.to_str().unwrap(),
"run",
));
assert!(
!h.source.contains("function nyxHeaderViaFixture(payload)"),
"fallback path must not emit the tier-(a) helper: {}",
h.source
);
assert!(
h.source.contains("const name = 'Set-Cookie';"),
"fallback path must emit the inline synthetic probe: {}",
h.source
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn emit_header_injection_harness_derives_entry_stem_from_entry_file() {
let dir = std::env::temp_dir().join("nyx_phase08_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 http = require('http');\nfunction run(res, value) { res.setHeader('Set-Cookie', encodeURIComponent(value)); }\nmodule.exports = { run };\n",
)
.unwrap();
let h = emit_header_injection_harness(&make_header_spec(
entry.to_str().unwrap(),
"run",
));
assert!(
h.source.contains("require('./benign')"),
"tier-(a) harness must require the staged fixture by its file_stem: {}",
h.source
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn emit_open_redirect_harness_routes_through_fixture_when_express_required() {
let dir = std::env::temp_dir().join("nyx_phase09_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 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 nyxRedirectViaFixture(payload)"),
"tier-(a) harness must define nyxRedirectViaFixture: {}",
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("redirect(...args)"),
"tier-(a) harness must define a res.redirect captor: {}",
h.source
);
assert!(
h.source.contains("if (String(name).toLowerCase() === 'location')"),
"tier-(a) harness must also capture setHeader('Location', …) writes: {}",
h.source
);
assert!(
h.source.contains("Synthetic fallback"),
"tier-(a) harness must preserve the inline probe as a fallback: {}",
h.source
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn emit_open_redirect_harness_falls_back_when_express_not_required() {
let dir = std::env::temp_dir().join("nyx_phase09_js_test_no_express");
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 nyxRedirectViaFixture(payload)"),
"fallback path must not emit the tier-(a) helper: {}",
h.source
);
assert!(
h.source.contains("const requestHost = 'example.com';"),
"fallback path must emit the inline synthetic probe: {}",
h.source
);
let _ = std::fs::remove_dir_all(&dir);
}
}