mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0013 (20260522T043516Z-29b8)
This commit is contained in:
parent
824a266303
commit
6f58921a17
4 changed files with 192 additions and 263 deletions
|
|
@ -1354,112 +1354,18 @@ pub fn emit_xpath_harness(spec: &HarnessSpec) -> HarnessSource {
|
|||
} else {
|
||||
spec.entry_name.clone()
|
||||
};
|
||||
let uses_real_xpath = entry_source.contains("javax.xml.xpath");
|
||||
|
||||
let main_body = if uses_real_xpath {
|
||||
format!(
|
||||
r#" // Phase 07 tier-(a): reflectively invoke the fixture's
|
||||
// `run(String)` so the real `javax.xml.xpath.XPath.evaluate`
|
||||
// call against the staged corpus document runs, then count
|
||||
// the returned `NodeList` nodes. Falls back to the inline
|
||||
// matcher when reflection fails so the harness still produces
|
||||
// a verdict on a fixture whose `run` signature does not match.
|
||||
int count = -1;
|
||||
try {{
|
||||
Class<?> entry = Class.forName("{entry_fqn}");
|
||||
Method m = entry.getDeclaredMethod("{entry_method}", String.class);
|
||||
m.setAccessible(true);
|
||||
Object result = m.invoke(null, payload);
|
||||
if (result instanceof NodeList) {{
|
||||
count = ((NodeList) result).getLength();
|
||||
}}
|
||||
}} catch (ClassNotFoundException | NoSuchMethodException
|
||||
| IllegalAccessException e) {{
|
||||
// Fixture shape did not match (String) -> NodeList — fall
|
||||
// through to the synthetic matcher below.
|
||||
}} catch (InvocationTargetException ite) {{
|
||||
// The fixture itself threw (malformed XPath, parse error,
|
||||
// ...); treat as a 0-node return so a benign fixture that
|
||||
// rejects the payload stays NotConfirmed.
|
||||
count = 0;
|
||||
}}
|
||||
if (count < 0) {{
|
||||
count = nyxXpathSelect(expr);
|
||||
}}"#
|
||||
)
|
||||
} else {
|
||||
r#" // No real javax.xml.xpath import on the entry source — fall
|
||||
// back to the inline matcher path so the harness still
|
||||
// produces a verdict.
|
||||
int count = nyxXpathSelect(expr);"#
|
||||
.to_owned()
|
||||
};
|
||||
|
||||
let imports = if uses_real_xpath {
|
||||
"import java.lang.reflect.InvocationTargetException;\n\
|
||||
import java.lang.reflect.Method;\n\
|
||||
import org.w3c.dom.NodeList;\n"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let source = format!(
|
||||
r#"// Nyx dynamic harness — XPATH_INJECTION javax.xml.xpath.XPath.evaluate (Phase 07 / Track J.5).
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
{imports}
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
public class NyxHarness {{
|
||||
{shim}
|
||||
|
||||
static final String[] NYX_XPATH_USERS = new String[] {{ "alice", "bob", "carol" }};
|
||||
|
||||
static int nyxXpathSelect(String expr) {{
|
||||
String needle = "//user[@name=";
|
||||
if (!expr.startsWith(needle)) return 0;
|
||||
String rest = expr.substring(needle.length());
|
||||
if (!rest.endsWith("]")) return 0;
|
||||
String predicate = rest.substring(0, rest.length() - 1);
|
||||
|
||||
Matcher single = Pattern.compile("^'([^']*)'(.*)$").matcher(predicate);
|
||||
if (single.find()) {{
|
||||
String literal = single.group(1);
|
||||
String tail = single.group(2).trim();
|
||||
if (tail.isEmpty() || tail.equals("]")) {{
|
||||
int count = 0;
|
||||
for (String u : NYX_XPATH_USERS) if (u.equals(literal)) count++;
|
||||
return count;
|
||||
}}
|
||||
if (Pattern.compile("^or\\s+", Pattern.CASE_INSENSITIVE).matcher(tail).find()) {{
|
||||
return NYX_XPATH_USERS.length;
|
||||
}}
|
||||
}}
|
||||
Matcher dbl = Pattern.compile("^\"([^\"]*)\"\\s*$").matcher(predicate);
|
||||
if (dbl.find()) {{
|
||||
String literal = dbl.group(1);
|
||||
int count = 0;
|
||||
for (String u : NYX_XPATH_USERS) if (u.equals(literal)) count++;
|
||||
return count;
|
||||
}}
|
||||
if (Pattern.compile("^concat\\(", Pattern.CASE_INSENSITIVE).matcher(predicate).find()) {{
|
||||
Matcher parts = Pattern.compile("'([^']*)'").matcher(predicate);
|
||||
StringBuilder joined = new StringBuilder();
|
||||
while (parts.find()) {{
|
||||
String p = parts.group(1);
|
||||
if (p.equals(",\"")) continue;
|
||||
joined.append(p);
|
||||
}}
|
||||
String result = joined.toString().replace(",\"'\",", "'");
|
||||
int count = 0;
|
||||
for (String u : NYX_XPATH_USERS) if (u.equals(result)) count++;
|
||||
return count;
|
||||
}}
|
||||
return NYX_XPATH_USERS.length;
|
||||
}}
|
||||
|
||||
static void nyxXpathProbe(String expr, int nodesReturned) {{
|
||||
String p = System.getenv("NYX_PROBE_PATH");
|
||||
if (p == null || p.isEmpty()) return;
|
||||
|
|
@ -1488,7 +1394,38 @@ public class NyxHarness {{
|
|||
String payload = System.getenv("NYX_PAYLOAD");
|
||||
if (payload == null) payload = "";
|
||||
String expr = "//user[@name='" + payload + "']";
|
||||
{main_body}
|
||||
// Phase 07 tier-(a): reflectively invoke the fixture's
|
||||
// `run(String)` so the real `javax.xml.xpath.XPath.evaluate`
|
||||
// call against the staged corpus document runs, then count
|
||||
// the returned `NodeList` nodes. Missing `javax.xml.xpath`
|
||||
// / `org.w3c.dom` on the JDK is the only structural reason
|
||||
// the reflective lookup fails; in that case we emit the
|
||||
// conventional `NYX_IMPORT_ERROR:` stderr marker plus
|
||||
// `System.exit(77)` so the runner maps the outcome to
|
||||
// `RunError::BuildFailed` and the e2e SKIP branch fires.
|
||||
int count;
|
||||
try {{
|
||||
Class<?> entry = Class.forName("{entry_fqn}");
|
||||
Method m = entry.getDeclaredMethod("{entry_method}", String.class);
|
||||
m.setAccessible(true);
|
||||
Object result = m.invoke(null, payload);
|
||||
if (result instanceof NodeList) {{
|
||||
count = ((NodeList) result).getLength();
|
||||
}} else {{
|
||||
count = 0;
|
||||
}}
|
||||
}} catch (ClassNotFoundException | NoSuchMethodException
|
||||
| IllegalAccessException e) {{
|
||||
System.err.println("NYX_IMPORT_ERROR: " + e.getClass().getName() + ": " + e.getMessage());
|
||||
System.exit(77);
|
||||
return;
|
||||
}} catch (InvocationTargetException ite) {{
|
||||
// The fixture itself threw (malformed XPath, parse error,
|
||||
// ...); treat as a 0-node return so a benign fixture that
|
||||
// rejects the payload stays NotConfirmed.
|
||||
count = 0;
|
||||
}}
|
||||
System.out.println("__NYX_XPATH_TIER_A__");
|
||||
nyxXpathProbe(expr, count);
|
||||
System.out.println("__NYX_SINK_HIT__");
|
||||
StringBuilder body = new StringBuilder(64);
|
||||
|
|
@ -3598,7 +3535,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn emit_xpath_harness_drives_fixture_through_real_xpath_when_imported() {
|
||||
fn emit_xpath_harness_routes_through_real_xpath_reflectively() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase07_test_drive_fixture");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
|
|
@ -3634,15 +3571,16 @@ mod tests {
|
|||
"tier-(a) harness must cast the result to NodeList and count nodes",
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("count = nyxXpathSelect(expr);"),
|
||||
"tier-(a) harness must preserve the inline matcher as a fallback",
|
||||
h.source.contains("__NYX_XPATH_TIER_A__"),
|
||||
"tier-(a) harness must emit the tier-(a) stdout marker after the real reflective invoke: {}",
|
||||
h.source
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_xpath_harness_falls_back_to_inline_matcher_without_xpath_import() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase07_test_no_xpath_import");
|
||||
fn emit_xpath_harness_drops_inline_matcher_fallback() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase07_test_no_inline_matcher");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let entry = write_servlet_fixture(
|
||||
|
|
@ -3655,16 +3593,27 @@ mod tests {
|
|||
spec.entry_name = "run".into();
|
||||
let h = emit_xpath_harness(&spec);
|
||||
assert!(
|
||||
!h.source.contains("import org.w3c.dom.NodeList;"),
|
||||
"fallback path must not import NodeList",
|
||||
!h.source.contains("nyxXpathSelect"),
|
||||
"harness must not carry the inline `nyxXpathSelect` matcher; tier-(a) reflective invoke is the only path",
|
||||
);
|
||||
assert!(
|
||||
!h.source.contains("Class.forName(\"Vuln\")"),
|
||||
"fallback path must not invoke the fixture reflectively",
|
||||
!h.source.contains("NYX_XPATH_USERS"),
|
||||
"harness must not carry the inline `NYX_XPATH_USERS` table; tier-(a) reflective invoke is the only path",
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("int count = nyxXpathSelect(expr);"),
|
||||
"fallback path must keep the inline matcher as the primary count source",
|
||||
h.source.contains("NYX_IMPORT_ERROR:") && h.source.contains("System.exit(77)"),
|
||||
"harness must emit `NYX_IMPORT_ERROR:` stderr marker + `System.exit(77)` on reflective lookup failure: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("__NYX_XPATH_TIER_A__"),
|
||||
"harness must emit the tier-(a) stdout marker: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("import org.w3c.dom.NodeList;")
|
||||
&& h.source.contains("import java.lang.reflect.Method;"),
|
||||
"harness must always import the reflective invocation path; the synthetic-only branch is gone",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1225,69 +1225,37 @@ 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}
|
||||
|
||||
const NYX_XPATH_USERS = ['alice', 'bob', 'carol'];
|
||||
|
||||
function nyxXpathSelect(expr) {{
|
||||
const needle = "//user[@name=";
|
||||
if (!expr.startsWith(needle)) return 0;
|
||||
const rest = expr.slice(needle.length);
|
||||
if (!rest.endsWith("]")) return 0;
|
||||
const predicate = rest.slice(0, -1);
|
||||
|
||||
let m = predicate.match(/^'([^']*)'(.*)$/);
|
||||
if (m) {{
|
||||
const literal = m[1];
|
||||
const tail = m[2].trim();
|
||||
if (tail === '' || tail === ']') {{
|
||||
return NYX_XPATH_USERS.filter((u) => u === literal).length;
|
||||
}}
|
||||
if (/^or\s+/i.test(tail)) {{
|
||||
return NYX_XPATH_USERS.length;
|
||||
}}
|
||||
}}
|
||||
m = predicate.match(/^"([^"]*)"\s*$/);
|
||||
if (m) {{
|
||||
const literal = m[1];
|
||||
return NYX_XPATH_USERS.filter((u) => u === literal).length;
|
||||
}}
|
||||
if (/^concat\(/i.test(predicate)) {{
|
||||
const parts = [...predicate.matchAll(/'([^']*)'/g)].map((x) => x[1]);
|
||||
let joined = parts.filter((p) => p !== ',"').join('');
|
||||
joined = joined.split(",\"'\",").join("'");
|
||||
return NYX_XPATH_USERS.filter((u) => u === joined).length;
|
||||
}}
|
||||
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.
|
||||
// the fixture chooses) runs against the staged corpus document. A
|
||||
// missing `xpath` host install is the only structural reason the
|
||||
// require fails; in that case we emit the conventional
|
||||
// `NYX_IMPORT_ERROR:` stderr marker plus `process.exit(77)` so the
|
||||
// runner maps the outcome to `RunError::BuildFailed` and the e2e
|
||||
// SKIP branch fires.
|
||||
let _entry;
|
||||
try {{
|
||||
_entry = require('./{entry_stem}');
|
||||
}} catch (e) {{
|
||||
return null;
|
||||
process.stderr.write('NYX_IMPORT_ERROR: ' + e.message + '\n');
|
||||
process.exit(77);
|
||||
}}
|
||||
const fn = _entry && (typeof _entry === 'function' ? _entry : _entry['{entry_name}']);
|
||||
if (typeof fn !== 'function') return null;
|
||||
if (typeof fn !== 'function') {{
|
||||
throw new Error("Phase 07 XPath harness: entry function '{entry_name}' not found in fixture module './{entry_stem}'");
|
||||
}}
|
||||
let result;
|
||||
try {{
|
||||
result = fn(payload);
|
||||
|
|
@ -1321,23 +1289,21 @@ function nyxXpathProbe(expr, nodesReturned) {{
|
|||
|
||||
const payload = process.env.NYX_PAYLOAD || '';
|
||||
const expr = "//user[@name='" + payload + "']";
|
||||
let nodes = nyxXpathViaFixture(payload);
|
||||
if (nodes === null) {{
|
||||
nodes = nyxXpathSelect(expr);
|
||||
}}
|
||||
const nodes = nyxXpathViaFixture(payload);
|
||||
console.log('__NYX_XPATH_TIER_A__');
|
||||
nyxXpathProbe(expr, nodes);
|
||||
console.log('__NYX_SINK_HIT__');
|
||||
console.log(JSON.stringify({{ expr: expr, nodes_returned: nodes }}));
|
||||
"#
|
||||
);
|
||||
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((
|
||||
let extra_files = vec![
|
||||
(corpus_filename.to_owned(), corpus_xml.to_owned()),
|
||||
("package.json".to_owned(), package_json_xpath()),
|
||||
(
|
||||
"package-lock.json".to_owned(),
|
||||
package_lock_skeleton("nyx-harness-xpath"),
|
||||
));
|
||||
}
|
||||
),
|
||||
];
|
||||
HarnessSource {
|
||||
source: body,
|
||||
filename: "harness.js".to_owned(),
|
||||
|
|
@ -2857,7 +2823,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn emit_xpath_harness_drives_fixture_through_real_xpath_when_imported() {
|
||||
fn emit_xpath_harness_routes_through_fixture_require() {
|
||||
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();
|
||||
|
|
@ -2891,34 +2857,34 @@ mod tests {
|
|||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("nodes = nyxXpathSelect(expr);"),
|
||||
"tier-(a) harness must preserve the inline matcher as a fallback: {}",
|
||||
h.source.contains("__NYX_XPATH_TIER_A__"),
|
||||
"harness must emit the tier-(a) stdout marker after the real xpath call: {}",
|
||||
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",
|
||||
"harness must always 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",
|
||||
"harness must always 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",
|
||||
"harness must always 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");
|
||||
fn emit_xpath_harness_drops_inline_matcher_fallback() {
|
||||
let dir = std::env::temp_dir().join("nyx_phase07_js_test_no_inline_matcher");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let entry = dir.join("vuln.js");
|
||||
|
|
@ -2929,14 +2895,28 @@ mod tests {
|
|||
.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)",
|
||||
!h.source.contains("nyxXpathSelect"),
|
||||
"harness must not carry the inline `nyxXpathSelect` matcher; tier-(a) is the only path",
|
||||
);
|
||||
assert!(
|
||||
!h.extra_files
|
||||
!h.source.contains("NYX_XPATH_USERS"),
|
||||
"harness must not carry the inline `NYX_XPATH_USERS` table; tier-(a) is the only path",
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("NYX_IMPORT_ERROR:") && h.source.contains("process.exit(77)"),
|
||||
"harness must emit `NYX_IMPORT_ERROR:` stderr marker + `process.exit(77)` on require failure: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("__NYX_XPATH_TIER_A__"),
|
||||
"harness must emit the tier-(a) stdout marker: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.extra_files
|
||||
.iter()
|
||||
.any(|(p, _)| p == "package-lock.json"),
|
||||
"fallback path must not stage a package-lock.json",
|
||||
.any(|(p, _)| p == "package.json"),
|
||||
"harness must always stage a package.json (real-xpath dep is required, no synthetic-only path)",
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1230,77 +1230,6 @@ pub fn emit_xpath_harness(spec: &HarnessSpec) -> HarnessSource {
|
|||
// Nyx dynamic harness — XPATH_INJECTION DOMXPath::query (Phase 07 / Track J.5).
|
||||
{shim}
|
||||
|
||||
// Synthetic in-process XPath evaluator over the canonical staged
|
||||
// document — counts <user> nodes that satisfy the `[@name='…']`
|
||||
// predicate the host code synthesised from the payload. Real
|
||||
// `DOMXPath::query` is not invoked (the harness ignores `_spec` and
|
||||
// inlines the evaluator); the differential rule still holds because
|
||||
// the vuln payload's `' or '1'='1` tail rewraps the selector into a
|
||||
// match-everything shape.
|
||||
$NYX_XPATH_USERS = ['alice', 'bob', 'carol'];
|
||||
|
||||
function _nyx_xpath_select($expr, array $users): int {{
|
||||
// Recognise the canonical `//user[@name='<payload>']` shape the
|
||||
// synthetic harness emits. Anything else falls through to "no
|
||||
// match" so a malformed expression cannot accidentally confirm.
|
||||
$needle = "//user[@name=";
|
||||
if (strncmp($expr, $needle, strlen($needle)) !== 0) {{
|
||||
return 0;
|
||||
}}
|
||||
$rest = substr($expr, strlen($needle));
|
||||
if (!str_ends_with($rest, ']')) {{
|
||||
return 0;
|
||||
}}
|
||||
$predicate = substr($rest, 0, strlen($rest) - 1);
|
||||
if (preg_match("/^'([^']*)'(.*)\$/", $predicate, $m)) {{
|
||||
// `name='alice'` → exact-match against the literal
|
||||
// `name='alice' or '1'='1'` → OR-tail breakouts; presence of
|
||||
// ` or ` after the closing quote means the selector is now
|
||||
// tautological → every user matches.
|
||||
$literal = $m[1];
|
||||
$tail = trim($m[2]);
|
||||
if ($tail === '' || $tail === ']') {{
|
||||
$count = 0;
|
||||
foreach ($users as $u) {{
|
||||
if ($u === $literal) $count++;
|
||||
}}
|
||||
return $count;
|
||||
}}
|
||||
if (preg_match("/^or\\s+/i", $tail)) {{
|
||||
return count($users);
|
||||
}}
|
||||
}}
|
||||
if (preg_match('/^"([^"]*)"\\s*$/', $predicate, $m)) {{
|
||||
$literal = $m[1];
|
||||
$count = 0;
|
||||
foreach ($users as $u) {{
|
||||
if ($u === $literal) $count++;
|
||||
}}
|
||||
return $count;
|
||||
}}
|
||||
if (preg_match("/^concat\\(/i", $predicate)) {{
|
||||
// `concat('a',\"'\",'b')` benign-escape path: extract the
|
||||
// joined literal and match exactly once.
|
||||
if (preg_match_all("/'([^']*)'/", $predicate, $parts)) {{
|
||||
$joined = '';
|
||||
foreach ($parts[1] as $p) {{
|
||||
if ($p === ',"') continue;
|
||||
$joined .= $p;
|
||||
}}
|
||||
// Normalise embedded single-quote literals back to the
|
||||
// raw character so a `concat`-quoted username collapses
|
||||
// to the same literal the user typed.
|
||||
$joined = str_replace(",\"'\",", "'", $joined);
|
||||
$count = 0;
|
||||
foreach ($users as $u) {{
|
||||
if ($u === $joined) $count++;
|
||||
}}
|
||||
return $count;
|
||||
}}
|
||||
}}
|
||||
return count($users);
|
||||
}}
|
||||
|
||||
function _nyx_xpath_probe(string $expr, int $nodes_returned): void {{
|
||||
$p = getenv('NYX_PROBE_PATH');
|
||||
if ($p === false || $p === '') return;
|
||||
|
|
@ -1315,23 +1244,34 @@ function _nyx_xpath_probe(string $expr, int $nodes_returned): void {{
|
|||
@file_put_contents($p, json_encode($rec) . "\n", FILE_APPEND);
|
||||
}}
|
||||
|
||||
function _nyx_xpath_via_fixture(string $payload, string $entry_basename, string $entry_name): ?int {{
|
||||
function _nyx_xpath_via_fixture(string $payload, string $entry_basename, string $entry_name): int {{
|
||||
// Phase 07 tier-(a): require the fixture file and call its
|
||||
// `$entry_name` function so the real `DOMXPath::query` runs
|
||||
// against the staged corpus document. Returns the result-set
|
||||
// length, or `null` when the require / call fails so the caller
|
||||
// can fall back to the inline matcher.
|
||||
// against the staged corpus document. A missing `ext-dom` /
|
||||
// `ext-xml` host install or an inaccessible fixture file is the
|
||||
// only structural reason this fails; in that case we emit the
|
||||
// conventional `NYX_IMPORT_ERROR:` stderr marker plus `exit(77)`
|
||||
// so the runner maps the outcome to `RunError::BuildFailed` and
|
||||
// the e2e SKIP branch fires.
|
||||
if (!class_exists('DOMDocument') || !class_exists('DOMXPath')) {{
|
||||
fwrite(STDERR, "NYX_IMPORT_ERROR: ext-dom / ext-xml not loaded\n");
|
||||
exit(77);
|
||||
}}
|
||||
$candidate = __DIR__ . DIRECTORY_SEPARATOR . $entry_basename;
|
||||
if (!is_file($candidate)) {{
|
||||
return null;
|
||||
fwrite(STDERR, "NYX_IMPORT_ERROR: fixture file not found at $candidate\n");
|
||||
exit(77);
|
||||
}}
|
||||
try {{
|
||||
require_once $candidate;
|
||||
}} catch (\Throwable $_) {{
|
||||
return null;
|
||||
}} catch (\Throwable $_e) {{
|
||||
fwrite(STDERR, "NYX_IMPORT_ERROR: " . $_e->getMessage() . "\n");
|
||||
exit(77);
|
||||
}}
|
||||
if (!function_exists($entry_name)) {{
|
||||
return null;
|
||||
throw new \RuntimeException(
|
||||
"Phase 07 XPath harness: entry function '$entry_name' not found in fixture '$entry_basename'"
|
||||
);
|
||||
}}
|
||||
try {{
|
||||
$result = $entry_name($payload);
|
||||
|
|
@ -1346,15 +1286,13 @@ function _nyx_xpath_via_fixture(string $payload, string $entry_basename, string
|
|||
if (is_array($result)) {{
|
||||
return count($result);
|
||||
}}
|
||||
return null;
|
||||
return 0;
|
||||
}}
|
||||
|
||||
$payload = (string) (getenv('NYX_PAYLOAD') ?: '');
|
||||
$expr = "//user[@name='" . $payload . "']";
|
||||
$nodes = _nyx_xpath_via_fixture($payload, "{entry_basename}", "{entry_name}");
|
||||
if ($nodes === null) {{
|
||||
$nodes = _nyx_xpath_select($expr, $NYX_XPATH_USERS);
|
||||
}}
|
||||
echo "__NYX_XPATH_TIER_A__\n";
|
||||
_nyx_xpath_probe($expr, $nodes);
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
echo json_encode(['expr' => $expr, 'nodes_returned' => $nodes]) . "\n";
|
||||
|
|
@ -2494,9 +2432,35 @@ mod tests {
|
|||
"PHP XPath harness must check the result against DOMNodeList",
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("__NYX_XPATH_TIER_A__"),
|
||||
"PHP XPath harness must emit the tier-(a) stdout marker after the real DOMXPath call: {}",
|
||||
h.source
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_xpath_harness_drops_inline_matcher_fallback() {
|
||||
let h = emit_xpath_harness(&make_xpath_spec(
|
||||
"tests/dynamic_fixtures/xpath_injection/php/vuln.php",
|
||||
"run",
|
||||
));
|
||||
assert!(
|
||||
!h.source.contains("_nyx_xpath_select"),
|
||||
"PHP XPath harness must not carry the inline `_nyx_xpath_select` matcher; tier-(a) is the only path",
|
||||
);
|
||||
assert!(
|
||||
!h.source.contains("NYX_XPATH_USERS"),
|
||||
"PHP XPath harness must not carry the inline `NYX_XPATH_USERS` table; tier-(a) is the only path",
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("NYX_IMPORT_ERROR:") && h.source.contains("exit(77)"),
|
||||
"PHP XPath harness must emit `NYX_IMPORT_ERROR:` stderr marker + `exit(77)` on require / ext failure: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("__NYX_XPATH_TIER_A__"),
|
||||
"PHP XPath harness must emit the tier-(a) stdout marker: {}",
|
||||
h.source
|
||||
.contains("$nodes = _nyx_xpath_select($expr, $NYX_XPATH_USERS);"),
|
||||
"PHP XPath harness must keep the inline matcher as a fallback",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -403,8 +403,11 @@ fn staged_corpus_carries_three_users() {
|
|||
// verdict path is deterministic without spawning a real XPath
|
||||
// engine (`stubs_required: vec![]`).
|
||||
//
|
||||
// JavaScript is skipped: the synthetic harness's `require('xpath')`
|
||||
// import resolves only when the workdir has the package installed.
|
||||
// Each lang asserts the tier-(a) stdout marker so a regression that
|
||||
// silently falls back to the inline matcher (now deleted) trips the
|
||||
// test; on hosts without the real engine installed the harness exits
|
||||
// 77 with `NYX_IMPORT_ERROR:` and `is_runtime_import_error` maps it to
|
||||
// `RunError::BuildFailed` (SKIP).
|
||||
|
||||
mod e2e_phase_07 {
|
||||
use crate::common::fixture_harness::FIXTURE_LOCK;
|
||||
|
|
@ -533,6 +536,17 @@ mod e2e_phase_07 {
|
|||
.as_ref()
|
||||
.expect("Confirmed run must carry a DifferentialOutcome");
|
||||
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
|
||||
let tier_a_marker = b"__NYX_XPATH_TIER_A__";
|
||||
let saw_tier_a = outcome.attempts.iter().any(|a| {
|
||||
a.outcome
|
||||
.stdout
|
||||
.windows(tier_a_marker.len())
|
||||
.any(|w| w == tier_a_marker)
|
||||
});
|
||||
assert!(
|
||||
saw_tier_a,
|
||||
"Java XPath vuln must reach the tier-(a) real-javax.xml.xpath path (stdout marker `__NYX_XPATH_TIER_A__`); the inline `nyxXpathSelect` fallback was removed and the harness now SKIPs via NYX_IMPORT_ERROR + System.exit(77) when the reflective lookup fails",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -576,6 +590,17 @@ mod e2e_phase_07 {
|
|||
.as_ref()
|
||||
.expect("Confirmed run must carry a DifferentialOutcome");
|
||||
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
|
||||
let tier_a_marker = b"__NYX_XPATH_TIER_A__";
|
||||
let saw_tier_a = outcome.attempts.iter().any(|a| {
|
||||
a.outcome
|
||||
.stdout
|
||||
.windows(tier_a_marker.len())
|
||||
.any(|w| w == tier_a_marker)
|
||||
});
|
||||
assert!(
|
||||
saw_tier_a,
|
||||
"PHP XPath vuln must reach the tier-(a) real-DOMXPath path (stdout marker `__NYX_XPATH_TIER_A__`); the inline `_nyx_xpath_select` fallback was removed and the harness now SKIPs via NYX_IMPORT_ERROR + exit 77 when ext-dom/ext-xml is unavailable",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -592,5 +617,16 @@ mod e2e_phase_07 {
|
|||
.as_ref()
|
||||
.expect("Confirmed run must carry a DifferentialOutcome");
|
||||
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
|
||||
let tier_a_marker = b"__NYX_XPATH_TIER_A__";
|
||||
let saw_tier_a = outcome.attempts.iter().any(|a| {
|
||||
a.outcome
|
||||
.stdout
|
||||
.windows(tier_a_marker.len())
|
||||
.any(|w| w == tier_a_marker)
|
||||
});
|
||||
assert!(
|
||||
saw_tier_a,
|
||||
"JavaScript XPath vuln must reach the tier-(a) real-xpath path (stdout marker `__NYX_XPATH_TIER_A__`); the inline `nyxXpathSelect` fallback was removed and the harness now SKIPs via NYX_IMPORT_ERROR + exit 77 when the `xpath` npm package is unavailable",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue