[pitboss] phase 07: Track J.5 + Track L.5 — XPATH_INJECTION corpus + XPath / DOM / lxml adapters

This commit is contained in:
pitboss 2026-05-17 23:47:12 -05:00
parent b2eeaabb09
commit a32075a756
38 changed files with 2111 additions and 67 deletions

View file

@ -20,7 +20,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[
bytes: b"alice*)(uid=*",
label: "ldap-java-filter-wildcard",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
@ -28,7 +28,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ldap_injection/java/Vuln.java"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
probe_predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
benign_control: Some(PayloadRef {
label: "ldap-java-benign",
}),
@ -38,7 +38,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[
bytes: b"alice",
label: "ldap-java-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,

View file

@ -15,7 +15,7 @@
//! intended single user.
//!
//! The oracle's
//! [`crate::dynamic::oracle::ProbePredicate::LdapResultCountGreaterThan`]
//! [`crate::dynamic::oracle::ProbePredicate::QueryResultCountGreaterThan`]
//! checks the per-payload `ProbeKind::Ldap.entries_returned` against
//! `n = 1` — vuln passes (3 entries), benign clears (1 entry),
//! fulfilling the §4.1 differential rule.

View file

@ -18,7 +18,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[
bytes: b"alice*)(uid=*",
label: "ldap-php-filter-wildcard",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
@ -26,7 +26,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ldap_injection/php/vuln.php"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
probe_predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
benign_control: Some(PayloadRef {
label: "ldap-php-benign",
}),
@ -36,7 +36,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[
bytes: b"alice",
label: "ldap-php-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,

View file

@ -19,7 +19,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[
bytes: b"alice*)(uid=*",
label: "ldap-python-filter-wildcard",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
@ -27,7 +27,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ldap_injection/python/vuln.py"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
probe_predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
benign_control: Some(PayloadRef {
label: "ldap-python-benign",
}),
@ -37,7 +37,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[
bytes: b"alice",
label: "ldap-python-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::LdapResultCountGreaterThan { n: 1 }],
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,

View file

@ -23,7 +23,7 @@
use std::collections::HashMap;
use std::sync::OnceLock;
use super::{cmdi, deserialize, fmt_string, ldap, path_trav, sqli, ssrf, ssti, xss, xxe};
use super::{cmdi, deserialize, fmt_string, ldap, path_trav, sqli, ssrf, ssti, xpath, xss, xxe};
use super::{CapCorpus, CuratedPayload, Oracle};
use crate::dynamic::oracle::ProbePredicate;
use crate::labels::Cap;
@ -40,7 +40,6 @@ pub const CORPUS_UNSUPPORTED_LANG_NEUTRAL: u32 = Cap::ENV_VAR.bits()
| Cap::CRYPTO.bits()
| Cap::UNAUTHORIZED_ID.bits()
| Cap::DATA_EXFIL.bits()
| Cap::XPATH_INJECTION.bits()
| Cap::HEADER_INJECTION.bits()
| Cap::OPEN_REDIRECT.bits()
| Cap::PROTOTYPE_POLLUTION.bits();
@ -71,6 +70,10 @@ const ENTRIES: &[(Cap, Lang, &[CuratedPayload])] = &[
(Cap::LDAP_INJECTION, Lang::Java, ldap::java::PAYLOADS),
(Cap::LDAP_INJECTION, Lang::Python, ldap::python::PAYLOADS),
(Cap::LDAP_INJECTION, Lang::Php, ldap::php::PAYLOADS),
(Cap::XPATH_INJECTION, Lang::Java, xpath::java::PAYLOADS),
(Cap::XPATH_INJECTION, Lang::Python, xpath::python::PAYLOADS),
(Cap::XPATH_INJECTION, Lang::Php, xpath::php::PAYLOADS),
(Cap::XPATH_INJECTION, Lang::JavaScript, xpath::js::PAYLOADS),
];
/// Reserved for per-cap oracle defaults. Empty in Phase 02; populated by
@ -281,6 +284,7 @@ mod tests {
assert!(!payloads_for(Cap::SSTI).is_empty());
assert!(!payloads_for(Cap::XXE).is_empty());
assert!(!payloads_for(Cap::LDAP_INJECTION).is_empty());
assert!(!payloads_for(Cap::XPATH_INJECTION).is_empty());
}
#[test]
@ -293,7 +297,6 @@ mod tests {
Cap::CRYPTO,
Cap::UNAUTHORIZED_ID,
Cap::DATA_EXFIL,
Cap::XPATH_INJECTION,
Cap::HEADER_INJECTION,
Cap::OPEN_REDIRECT,
Cap::PROTOTYPE_POLLUTION,
@ -328,6 +331,7 @@ mod tests {
Cap::SSTI,
Cap::XXE,
Cap::LDAP_INJECTION,
Cap::XPATH_INJECTION,
] {
let has_vuln = payloads_for(cap).iter().any(|p| !p.is_benign);
assert!(has_vuln, "{cap:?} must have at least one vuln payload");
@ -378,6 +382,7 @@ mod tests {
Cap::SSTI,
Cap::XXE,
Cap::LDAP_INJECTION,
Cap::XPATH_INJECTION,
];
for cap in caps {
for p in payloads_for(cap) {
@ -403,6 +408,7 @@ mod tests {
Cap::SSTI,
Cap::XXE,
Cap::LDAP_INJECTION,
Cap::XPATH_INJECTION,
];
for cap in caps {
for p in payloads_for(cap) {
@ -515,6 +521,7 @@ mod tests {
Cap::SSTI,
Cap::XXE,
Cap::LDAP_INJECTION,
Cap::XPATH_INJECTION,
];
for cap in caps {
for p in payloads_for(cap).iter().filter(|p| p.is_benign) {
@ -726,6 +733,48 @@ mod tests {
}
}
#[test]
fn xpath_has_per_lang_slices_for_phase_07() {
// Phase 07 (Track J.5) acceptance: XPATH_INJECTION registers
// payloads in Java / Python / PHP / JavaScript and the
// lang-aware lookup never returns empty for any of them.
for lang in [Lang::Java, Lang::Python, Lang::Php, Lang::JavaScript] {
assert!(
!payloads_for_lang(Cap::XPATH_INJECTION, lang).is_empty(),
"XPATH_INJECTION must have at least one payload for {lang:?}",
);
}
// Rust / C / Cpp / Ruby / Go / TS not yet covered.
for lang in [
Lang::Rust,
Lang::C,
Lang::Cpp,
Lang::Ruby,
Lang::Go,
Lang::TypeScript,
] {
assert!(
payloads_for_lang(Cap::XPATH_INJECTION, lang).is_empty(),
"XPATH_INJECTION has unexpected payloads for {lang:?}",
);
}
}
#[test]
fn xpath_payloads_pair_benign_controls_per_lang() {
for lang in [Lang::Java, Lang::Python, Lang::Php, Lang::JavaScript] {
let slice = payloads_for_lang(Cap::XPATH_INJECTION, lang);
let vuln = slice
.iter()
.find(|p| !p.is_benign)
.expect("each lang must have an XPath vuln payload");
let resolved =
super::resolve_benign_control_lang(vuln, Cap::XPATH_INJECTION, lang)
.expect("lang-aware benign control must resolve");
assert!(resolved.is_benign);
}
}
#[test]
fn deserialize_payloads_pair_benign_controls_per_lang() {
// The lang-aware resolver must find the paired benign control

View file

@ -0,0 +1,53 @@
//! Java `Cap::XPATH_INJECTION` payloads — `javax.xml.xpath.XPath.evaluate`
//! expression injection.
//!
//! Vuln payload: an XPath fragment whose `' or '1'='1` tail breaks
//! out of the host template's `[@name='…']` predicate and rewraps
//! the selector as `//user[@name='' or '1'='1']`, matching every
//! node the staged document carries. The harness's instrumented
//! `XPath.evaluate` records
//! `ProbeKind::Xpath { nodes_returned: 3 }`.
//!
//! Benign control: the same intended username quoted via the
//! harness's XPath-escape helper, leaving the expression pinned to a
//! single node — `nodes_returned: 1`, oracle clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"alice' or '1'='1",
label: "xpath-java-expression-wildcard",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 11,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/xpath_injection/java/Vuln.java"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
benign_control: Some(PayloadRef {
label: "xpath-java-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "xpath-java-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 11,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/xpath_injection/java/Benign.java"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,53 @@
//! JavaScript `Cap::XPATH_INJECTION` payloads — `xpath` npm package's
//! `select` expression injection.
//!
//! Vuln payload: an XPath fragment whose `' or '1'='1` tail breaks
//! out of the host template's `[@name='…']` predicate; the
//! synthesized expression becomes `//user[@name='' or '1'='1']` and
//! matches every node in the staged document. The harness's
//! instrumented `xpath.select` records
//! `ProbeKind::Xpath { nodes_returned: 3 }`.
//!
//! Benign control: the same intended username quoted via the
//! harness's XPath-escape helper, leaving the expression pinned to a
//! single node — `nodes_returned: 1`, oracle clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"alice' or '1'='1",
label: "xpath-js-expression-wildcard",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 11,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/xpath_injection/js/vuln.js"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
benign_control: Some(PayloadRef {
label: "xpath-js-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "xpath-js-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 11,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/xpath_injection/js/benign.js"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,29 @@
//! XPath expression injection (`Cap::XPATH_INJECTION`) per-language
//! payload slices.
//!
//! Phase 07 (Track J.5) carves XPath injection across the four
//! most-common XPath evaluator stacks: Java
//! (`javax.xml.xpath.XPath.evaluate`), Python (`lxml.etree.xpath`),
//! PHP (`DOMXPath::query`), and Node.js (`xpath` npm package's
//! `select`). Every vuln payload appends the canonical
//! `' or '1'='1` quote-escape break — once the host code substitutes
//! the attacker bytes into its XPath template the synthesized
//! expression selects every node the in-workdir
//! [`crate::dynamic::stubs::xpath_document`] XML carries (three
//! users). The paired benign control quotes the same bytes through
//! the per-language escape helper, leaving the expression pinned to
//! the originally-intended single node.
//!
//! The oracle's
//! [`crate::dynamic::oracle::ProbePredicate::QueryResultCountGreaterThan`]
//! checks the per-payload `ProbeKind::Xpath.nodes_returned` against
//! `n = 1` — vuln passes (3 nodes), benign clears (1 node),
//! fulfilling the §4.1 differential rule. The same predicate also
//! satisfies LDAP probes (`ProbeKind::Ldap.entries_returned`); the
//! Phase 06 → Phase 07 rename from `LdapResultCountGreaterThan` to
//! `QueryResultCountGreaterThan` captures the shared shape.
pub mod java;
pub mod js;
pub mod php;
pub mod python;

View file

@ -0,0 +1,53 @@
//! PHP `Cap::XPATH_INJECTION` payloads — `DOMXPath::query` expression
//! injection.
//!
//! Vuln payload: an XPath fragment whose `' or '1'='1` tail breaks
//! out of the host template's `[@name='…']` predicate; the
//! synthesized expression becomes `//user[@name='' or '1'='1']` and
//! matches every node in the staged document. The harness's
//! instrumented `DOMXPath::query` records
//! `ProbeKind::Xpath { nodes_returned: 3 }`.
//!
//! Benign control: the same intended username quoted via the
//! harness's XPath-escape helper, leaving the expression pinned to a
//! single node — `nodes_returned: 1`, oracle clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"alice' or '1'='1",
label: "xpath-php-expression-wildcard",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 11,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/xpath_injection/php/vuln.php"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
benign_control: Some(PayloadRef {
label: "xpath-php-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "xpath-php-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 11,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/xpath_injection/php/benign.php"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,53 @@
//! Python `Cap::XPATH_INJECTION` payloads — `lxml.etree.xpath`
//! expression injection.
//!
//! Vuln payload: an XPath fragment whose `' or '1'='1` tail breaks
//! out of the host template's `[@name='…']` predicate; the
//! synthesized expression becomes `//user[@name='' or '1'='1']` and
//! matches every node in the staged document. The harness's
//! instrumented `xpath` evaluator records
//! `ProbeKind::Xpath { nodes_returned: 3 }`.
//!
//! Benign control: the same intended username quoted via the
//! harness's XPath-escape helper, leaving the expression pinned to a
//! single node — `nodes_returned: 1`, oracle clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"alice' or '1'='1",
label: "xpath-python-expression-wildcard",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 11,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/xpath_injection/python/vuln.py"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
benign_control: Some(PayloadRef {
label: "xpath-python-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "xpath-python-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 11,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/xpath_injection/python/benign.py"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];