mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-18 20:15:14 +02:00
[pitboss] phase 10: Track J.8 + Track L.8 — PROTOTYPE_POLLUTION corpus + JS/TS prototype chain hook
This commit is contained in:
parent
97e4dfff30
commit
d8f88d97bb
20 changed files with 1406 additions and 22 deletions
64
src/dynamic/corpus/prototype_pollution/javascript.rs
Normal file
64
src/dynamic/corpus/prototype_pollution/javascript.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
//! JavaScript `Cap::PROTOTYPE_POLLUTION` payloads.
|
||||
//!
|
||||
//! Vuln payload: a JSON literal whose top-level key is `__proto__`.
|
||||
//! When the host's deep-merge / `Object.assign` / JSON-parse-then-
|
||||
//! deep-assign sink walks the key into the merge target, the
|
||||
//! `__proto__` reference reaches `Object.prototype` and writes
|
||||
//! `__nyx_canary`. The harness's instrumented setter trap records a
|
||||
//! `ProbeKind::PrototypePollution { property: "__nyx_canary", … }`
|
||||
//! probe; the predicate
|
||||
//! [`crate::dynamic::oracle::ProbePredicate::PrototypeCanaryTouched`]
|
||||
//! fires on the captured canary write.
|
||||
//!
|
||||
//! Benign control: a JSON literal whose top-level key is the regular
|
||||
//! property `data`. Naive deep-merge walks `target.data` rather than
|
||||
//! `target.__proto__`, so the prototype chain stays untouched and the
|
||||
//! predicate stays clear. The matching fixture
|
||||
//! (`tests/dynamic_fixtures/prototype_pollution/javascript/benign.js`)
|
||||
//! additionally demonstrates the canonical mitigation pattern of
|
||||
//! using `Object.create(null)` as the merge target — static analysis
|
||||
//! of the fixture surfaces the prototype-less target while the
|
||||
//! runtime differential is driven by the absence of a `__proto__` key
|
||||
//! in the payload bytes.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
const CANARY: &str = "__nyx_canary";
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: br#"{"__proto__":{"__nyx_canary":"pwned"}}"#,
|
||||
label: "prototype-pollution-js-proto-key",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 14,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/prototype_pollution/javascript/vuln.js"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "prototype-pollution-js-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: br#"{"data":{"__nyx_canary":"pwned"}}"#,
|
||||
label: "prototype-pollution-js-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 14,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/prototype_pollution/javascript/benign.js"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
20
src/dynamic/corpus/prototype_pollution/mod.rs
Normal file
20
src/dynamic/corpus/prototype_pollution/mod.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
//! Prototype-pollution (`Cap::PROTOTYPE_POLLUTION`) per-language
|
||||
//! payload slices.
|
||||
//!
|
||||
//! Phase 10 (Track J.8) carves the JavaScript / TypeScript prototype-
|
||||
//! pollution gadget against three sink families: `lodash.merge`,
|
||||
//! `Object.assign` with tainted RHS, and `JSON.parse`-then-deep-assign.
|
||||
//! Every vuln payload binds a JSON literal whose top-level key is
|
||||
//! `__proto__`; the harness's instrumented deep-merge walks the key
|
||||
//! into `Object.prototype` and a `Proxy`-style setter trap on
|
||||
//! `Object.prototype.__nyx_canary` records a
|
||||
//! [`crate::dynamic::probe::ProbeKind::PrototypePollution`] probe. The
|
||||
//! paired benign control sends a JSON literal whose top-level key is
|
||||
//! the regular property `data`, leaving the prototype chain
|
||||
//! untouched. The
|
||||
//! [`crate::dynamic::oracle::ProbePredicate::PrototypeCanaryTouched`]
|
||||
//! predicate fires only on probes whose `property` equals the canary
|
||||
//! name (`__nyx_canary`).
|
||||
|
||||
pub mod javascript;
|
||||
pub mod typescript;
|
||||
50
src/dynamic/corpus/prototype_pollution/typescript.rs
Normal file
50
src/dynamic/corpus/prototype_pollution/typescript.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
//! TypeScript `Cap::PROTOTYPE_POLLUTION` payloads.
|
||||
//!
|
||||
//! Mirrors [`super::javascript`] — the runtime is Node.js in both
|
||||
//! cases, so the payload shape and oracle predicate are identical.
|
||||
//! The per-language slice exists so the lang-aware corpus resolver
|
||||
//! pairs TS vuln payloads against TS benign controls without crossing
|
||||
//! the JS slice (and so the fixture paths point at the TS-specific
|
||||
//! fixtures the static-analysis side consumes).
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
const CANARY: &str = "__nyx_canary";
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: br#"{"__proto__":{"__nyx_canary":"pwned"}}"#,
|
||||
label: "prototype-pollution-ts-proto-key",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 14,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/prototype_pollution/typescript/vuln.ts"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "prototype-pollution-ts-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: br#"{"data":{"__nyx_canary":"pwned"}}"#,
|
||||
label: "prototype-pollution-ts-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 14,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/prototype_pollution/typescript/benign.ts"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
|
|
@ -24,8 +24,8 @@ use std::collections::HashMap;
|
|||
use std::sync::OnceLock;
|
||||
|
||||
use super::{
|
||||
cmdi, deserialize, fmt_string, header_injection, ldap, open_redirect, path_trav, sqli, ssrf,
|
||||
ssti, xpath, xss, xxe,
|
||||
cmdi, deserialize, fmt_string, header_injection, ldap, open_redirect, path_trav,
|
||||
prototype_pollution, sqli, ssrf, ssti, xpath, xss, xxe,
|
||||
};
|
||||
use super::{CapCorpus, CuratedPayload, Oracle};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
|
@ -42,8 +42,7 @@ pub const CORPUS_UNSUPPORTED_LANG_NEUTRAL: u32 = Cap::ENV_VAR.bits()
|
|||
| Cap::JSON_PARSE.bits()
|
||||
| Cap::CRYPTO.bits()
|
||||
| Cap::UNAUTHORIZED_ID.bits()
|
||||
| Cap::DATA_EXFIL.bits()
|
||||
| Cap::PROTOTYPE_POLLUTION.bits();
|
||||
| Cap::DATA_EXFIL.bits();
|
||||
|
||||
/// Flat `(Cap, Lang, slice)` table. A single cap can carry per-language
|
||||
/// variants — that's the whole reason this layer exists.
|
||||
|
|
@ -89,6 +88,16 @@ const ENTRIES: &[(Cap, Lang, &[CuratedPayload])] = &[
|
|||
(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),
|
||||
(
|
||||
Cap::PROTOTYPE_POLLUTION,
|
||||
Lang::JavaScript,
|
||||
prototype_pollution::javascript::PAYLOADS,
|
||||
),
|
||||
(
|
||||
Cap::PROTOTYPE_POLLUTION,
|
||||
Lang::TypeScript,
|
||||
prototype_pollution::typescript::PAYLOADS,
|
||||
),
|
||||
];
|
||||
|
||||
/// Reserved for per-cap oracle defaults. Empty in Phase 02; populated by
|
||||
|
|
@ -302,6 +311,7 @@ mod tests {
|
|||
assert!(!payloads_for(Cap::XPATH_INJECTION).is_empty());
|
||||
assert!(!payloads_for(Cap::HEADER_INJECTION).is_empty());
|
||||
assert!(!payloads_for(Cap::OPEN_REDIRECT).is_empty());
|
||||
assert!(!payloads_for(Cap::PROTOTYPE_POLLUTION).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -314,7 +324,6 @@ mod tests {
|
|||
Cap::CRYPTO,
|
||||
Cap::UNAUTHORIZED_ID,
|
||||
Cap::DATA_EXFIL,
|
||||
Cap::PROTOTYPE_POLLUTION,
|
||||
];
|
||||
for cap in unsupported {
|
||||
assert!(
|
||||
|
|
@ -349,6 +358,7 @@ mod tests {
|
|||
Cap::XPATH_INJECTION,
|
||||
Cap::HEADER_INJECTION,
|
||||
Cap::OPEN_REDIRECT,
|
||||
Cap::PROTOTYPE_POLLUTION,
|
||||
] {
|
||||
let has_vuln = payloads_for(cap).iter().any(|p| !p.is_benign);
|
||||
assert!(has_vuln, "{cap:?} must have at least one vuln payload");
|
||||
|
|
@ -402,6 +412,7 @@ mod tests {
|
|||
Cap::XPATH_INJECTION,
|
||||
Cap::HEADER_INJECTION,
|
||||
Cap::OPEN_REDIRECT,
|
||||
Cap::PROTOTYPE_POLLUTION,
|
||||
];
|
||||
for cap in caps {
|
||||
for p in payloads_for(cap) {
|
||||
|
|
@ -430,6 +441,7 @@ mod tests {
|
|||
Cap::XPATH_INJECTION,
|
||||
Cap::HEADER_INJECTION,
|
||||
Cap::OPEN_REDIRECT,
|
||||
Cap::PROTOTYPE_POLLUTION,
|
||||
];
|
||||
for cap in caps {
|
||||
for p in payloads_for(cap) {
|
||||
|
|
@ -545,6 +557,7 @@ mod tests {
|
|||
Cap::XPATH_INJECTION,
|
||||
Cap::HEADER_INJECTION,
|
||||
Cap::OPEN_REDIRECT,
|
||||
Cap::PROTOTYPE_POLLUTION,
|
||||
];
|
||||
for cap in caps {
|
||||
for p in payloads_for(cap).iter().filter(|p| p.is_benign) {
|
||||
|
|
@ -849,6 +862,50 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prototype_pollution_has_per_lang_slices_for_phase_10() {
|
||||
// Phase 10 (Track J.8) acceptance: PROTOTYPE_POLLUTION
|
||||
// registers payloads in JavaScript / TypeScript and the
|
||||
// lang-aware lookup never returns empty for either.
|
||||
for lang in [Lang::JavaScript, Lang::TypeScript] {
|
||||
assert!(
|
||||
!payloads_for_lang(Cap::PROTOTYPE_POLLUTION, lang).is_empty(),
|
||||
"PROTOTYPE_POLLUTION must have at least one payload for {lang:?}",
|
||||
);
|
||||
}
|
||||
// Other langs not covered.
|
||||
for lang in [
|
||||
Lang::Rust,
|
||||
Lang::C,
|
||||
Lang::Cpp,
|
||||
Lang::Go,
|
||||
Lang::Java,
|
||||
Lang::Php,
|
||||
Lang::Python,
|
||||
Lang::Ruby,
|
||||
] {
|
||||
assert!(
|
||||
payloads_for_lang(Cap::PROTOTYPE_POLLUTION, lang).is_empty(),
|
||||
"PROTOTYPE_POLLUTION has unexpected payloads for {lang:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prototype_pollution_payloads_pair_benign_controls_per_lang() {
|
||||
for lang in [Lang::JavaScript, Lang::TypeScript] {
|
||||
let slice = payloads_for_lang(Cap::PROTOTYPE_POLLUTION, lang);
|
||||
let vuln = slice
|
||||
.iter()
|
||||
.find(|p| !p.is_benign)
|
||||
.expect("each lang must have a PROTOTYPE_POLLUTION vuln payload");
|
||||
let resolved =
|
||||
super::resolve_benign_control_lang(vuln, Cap::PROTOTYPE_POLLUTION, lang)
|
||||
.expect("lang-aware benign control must resolve");
|
||||
assert!(resolved.is_benign);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_payloads_pair_benign_controls_per_lang() {
|
||||
// The lang-aware resolver must find the paired benign control
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue