mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
feat(dynamic): implement entry-driven verification with fallback to synthetic direct-sink, enhance per-language emitters, and improve test coverage
This commit is contained in:
parent
130bf904eb
commit
738f1fedbc
9 changed files with 686 additions and 116 deletions
|
|
@ -182,6 +182,79 @@ fn lang_emitter_dispatches_to_deserialize_harness() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_harness_drives_entry_when_derivable() {
|
||||
// Java: reflectively load the fixture class and invoke the derived
|
||||
// entry method so the fixture's own resolveClass allowlist runs before
|
||||
// the gadget class resolves.
|
||||
let java = lang::emit(&make_spec(
|
||||
Lang::Java,
|
||||
"tests/dynamic_fixtures/deserialize/java/Benign.java",
|
||||
"run",
|
||||
))
|
||||
.expect("java deser emit");
|
||||
assert!(
|
||||
java.source.contains("Class.forName(\"Benign\")"),
|
||||
"Java deser harness must reflectively load the fixture class",
|
||||
);
|
||||
assert!(
|
||||
java.source.contains("getMethod(\"run\""),
|
||||
"Java deser harness must invoke the derived entry method",
|
||||
);
|
||||
assert!(
|
||||
java.source.contains("nyxCauseChainHas"),
|
||||
"Java deser harness must detect gadget resolution via the cause chain",
|
||||
);
|
||||
|
||||
// Ruby: require_relative the fixture and drive its entry so the
|
||||
// const-name guard runs before Marshal.load.
|
||||
let ruby = lang::emit(&make_spec(
|
||||
Lang::Ruby,
|
||||
"tests/dynamic_fixtures/deserialize/ruby/benign.rb",
|
||||
"run",
|
||||
))
|
||||
.expect("ruby deser emit");
|
||||
assert!(
|
||||
ruby.source.contains("require_relative './benign'"),
|
||||
"Ruby deser harness must require_relative the fixture",
|
||||
);
|
||||
assert!(
|
||||
ruby.source.contains("__send__(:'run'"),
|
||||
"Ruby deser harness must drive the derived entry function",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_harness_falls_back_to_synthetic_without_entry() {
|
||||
// No derivable enclosing entry → direct-sink synthetic path; the
|
||||
// harness must not attempt to load a fixture it cannot name.
|
||||
let java = lang::emit(&make_spec(
|
||||
Lang::Java,
|
||||
"tests/dynamic_fixtures/deserialize/java/Vuln.java",
|
||||
"<unknown>",
|
||||
))
|
||||
.expect("java deser emit");
|
||||
assert!(
|
||||
!java.source.contains("Class.forName("),
|
||||
"Java deser harness must not reflect into a fixture when no entry is derivable",
|
||||
);
|
||||
assert!(
|
||||
java.source.contains("nyxSyntheticDeserialize"),
|
||||
"Java synthetic fallback must drive the restricted-OIS path directly",
|
||||
);
|
||||
|
||||
let ruby = lang::emit(&make_spec(
|
||||
Lang::Ruby,
|
||||
"tests/dynamic_fixtures/deserialize/ruby/vuln.rb",
|
||||
"<unknown>",
|
||||
))
|
||||
.expect("ruby deser emit");
|
||||
assert!(
|
||||
!ruby.source.contains("require_relative"),
|
||||
"Ruby deser harness must not require the fixture when no entry is derivable",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn framework_adapters_detect_deserialize_sink() {
|
||||
// Java + Python + PHP + Ruby all register their J.1 sink adapter;
|
||||
|
|
|
|||
|
|
@ -418,12 +418,13 @@ fn slug(lang: Lang) -> &'static str {
|
|||
// into the prototype chain.
|
||||
//
|
||||
// Per-lang skips mirror the Phase 08 e2e block:
|
||||
// - TypeScript: the synthetic harness short-circuits the entry
|
||||
// source load entirely (`entry_subpath: None`), so no `tsx` /
|
||||
// `ts-node` is needed at runtime — but on hosts without
|
||||
// `tree_sitter_typescript` or the npm Node toolchain, the
|
||||
// harness build will fall through `BuildFailed` and skip via the
|
||||
// same branch.
|
||||
// - TypeScript: the entry-driven harness now loads the fixture
|
||||
// through an in-harness type-stripping + ESM→CJS shim
|
||||
// (`nyxLoadTsEntry`), so no `tsx` / `ts-node` is needed at
|
||||
// runtime — but on hosts without `tree_sitter_typescript`, a Node
|
||||
// build lacking `module.stripTypeScriptTypes`, or the npm Node
|
||||
// toolchain, the harness build/load falls through `BuildFailed`
|
||||
// (or the runtime tier-(b) fallback) and skips via the same branch.
|
||||
|
||||
mod e2e_phase_10 {
|
||||
use crate::common::fixture_harness::FIXTURE_LOCK;
|
||||
|
|
@ -540,6 +541,25 @@ mod e2e_phase_10 {
|
|||
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
|
||||
}
|
||||
|
||||
/// A benign control must NOT confirm: the entry-driven harness invokes
|
||||
/// the fixture's own `run`, whose `Object.create(null)` merge target
|
||||
/// keeps the `__proto__` payload off the shared prototype, so the
|
||||
/// canary trap stays clear and the differential never confirms.
|
||||
fn assert_not_confirmed(lang: Lang, outcome: &RunOutcome) {
|
||||
assert!(
|
||||
outcome.triggered_by.is_none(),
|
||||
"{lang:?} PROTOTYPE_POLLUTION benign control must NOT confirm — the \
|
||||
caller-side `Object.create(null)` guard must participate; got {outcome:?}",
|
||||
);
|
||||
if let Some(diff) = outcome.differential.as_ref() {
|
||||
assert_ne!(
|
||||
diff.verdict,
|
||||
DifferentialVerdict::Confirmed,
|
||||
"{lang:?} benign differential must not be Confirmed",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_vuln_confirms_via_run_spec() {
|
||||
let Some(outcome) = run(Lang::JavaScript, "vuln.js", "run") else {
|
||||
|
|
@ -555,4 +575,20 @@ mod e2e_phase_10 {
|
|||
};
|
||||
assert_confirmed(Lang::TypeScript, &outcome);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_benign_not_confirmed_via_run_spec() {
|
||||
let Some(outcome) = run(Lang::JavaScript, "benign.js", "run") else {
|
||||
return;
|
||||
};
|
||||
assert_not_confirmed(Lang::JavaScript, &outcome);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ts_benign_not_confirmed_via_run_spec() {
|
||||
let Some(outcome) = run(Lang::TypeScript, "benign.ts", "run") else {
|
||||
return;
|
||||
};
|
||||
assert_not_confirmed(Lang::TypeScript, &outcome);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue