mirror of
https://github.com/elicpeter/nyx.git
synced 2026-07-06 20:42:10 +02:00
Dynamic (#77)
This commit is contained in:
parent
55247b7fcd
commit
991c84a1eb
1464 changed files with 225448 additions and 1985 deletions
226
tests/marker_uniqueness.rs
Normal file
226
tests/marker_uniqueness.rs
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
#![allow(deprecated)]
|
||||
//! Marker uniqueness test (§4.1, §17.4).
|
||||
//!
|
||||
//! Asserts that no `NYX_PWN_*` marker from one cap's corpus is a substring
|
||||
//! of any other cap's payloads, expected sanitizer outputs, or §17.4
|
||||
//! redactor patterns.
|
||||
//!
|
||||
//! This prevents oracle collisions where a SQLi payload accidentally
|
||||
//! triggers the CMDi oracle (or vice versa), producing false `Confirmed`
|
||||
//! verdicts.
|
||||
//!
|
||||
//! Tests are gated on `#[cfg(feature = "dynamic")]` because the corpus
|
||||
//! module lives under the `dynamic` feature.
|
||||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
use nyx_scanner::dynamic::corpus::payloads_for;
|
||||
use nyx_scanner::labels::Cap;
|
||||
|
||||
/// All markers extracted from non-benign payload oracles, tagged with the cap
|
||||
/// they came from.
|
||||
fn oracle_markers() -> Vec<(Cap, &'static str, &'static [u8])> {
|
||||
let mut markers = Vec::new();
|
||||
for cap in [
|
||||
Cap::SQL_QUERY,
|
||||
Cap::CODE_EXEC,
|
||||
Cap::FILE_IO,
|
||||
Cap::SSRF,
|
||||
Cap::HTML_ESCAPE,
|
||||
] {
|
||||
for payload in payloads_for(cap).iter().filter(|p| !p.is_benign) {
|
||||
if let nyx_scanner::dynamic::corpus::Oracle::OutputContains(marker) = payload.oracle {
|
||||
markers.push((cap, marker, payload.bytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
markers
|
||||
}
|
||||
|
||||
/// Redactor patterns from §17.4 (the literal strings that trigger redaction).
|
||||
const REDACTOR_PREFIXES: &[&str] = &[
|
||||
"AKIA",
|
||||
"ghp_",
|
||||
"github_pat_",
|
||||
"ghs_",
|
||||
"ghr_",
|
||||
"xoxa-",
|
||||
"xoxb-",
|
||||
"xoxp-",
|
||||
"xoxr-",
|
||||
"sk-",
|
||||
"-----BEGIN",
|
||||
"password=",
|
||||
"api_key=",
|
||||
"api_token=",
|
||||
"secret=",
|
||||
"Bearer ",
|
||||
];
|
||||
|
||||
/// Expected sanitizer outputs (strings that appear after correct sanitization).
|
||||
/// These must NOT appear in any payload oracle marker.
|
||||
const EXPECTED_SANITIZED_OUTPUTS: &[&str] = &[
|
||||
"<script>",
|
||||
">",
|
||||
"<",
|
||||
"&",
|
||||
"'",
|
||||
"%27",
|
||||
"\\u003c",
|
||||
"\\u003e",
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn no_marker_is_substring_of_another_caps_payload() {
|
||||
let markers = oracle_markers();
|
||||
|
||||
// For each marker, check it does not appear in another cap's payloads.
|
||||
let caps = [
|
||||
Cap::SQL_QUERY,
|
||||
Cap::CODE_EXEC,
|
||||
Cap::FILE_IO,
|
||||
Cap::SSRF,
|
||||
Cap::HTML_ESCAPE,
|
||||
];
|
||||
|
||||
let mut violations: Vec<String> = Vec::new();
|
||||
|
||||
for (src_cap, marker_str, _marker_src_payload) in &markers {
|
||||
let marker_bytes = marker_str.as_bytes();
|
||||
|
||||
for cap in caps {
|
||||
// Within-cap reuse is allowed per §4.1 (cap A's marker may appear
|
||||
// in cap A's own payloads); only cross-cap appearance is a collision.
|
||||
if cap == *src_cap {
|
||||
continue;
|
||||
}
|
||||
for payload in payloads_for(cap).iter().filter(|p| !p.is_benign) {
|
||||
let payload_contains_marker = payload
|
||||
.bytes
|
||||
.windows(marker_bytes.len())
|
||||
.any(|w| w == marker_bytes);
|
||||
|
||||
if payload_contains_marker {
|
||||
violations.push(format!(
|
||||
"marker {:?} (from cap {:?}) appears as substring in payload {:?} (cap {:?})",
|
||||
marker_str,
|
||||
src_cap,
|
||||
payload.label,
|
||||
cap,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
violations.is_empty(),
|
||||
"Marker uniqueness violation(s):\n{}",
|
||||
violations.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_marker_is_substring_of_sanitized_output() {
|
||||
let markers = oracle_markers();
|
||||
|
||||
let mut violations: Vec<String> = Vec::new();
|
||||
|
||||
for (_, marker, _) in &markers {
|
||||
for sanitized in EXPECTED_SANITIZED_OUTPUTS {
|
||||
if sanitized.contains(marker) || marker.contains(sanitized) {
|
||||
violations.push(format!(
|
||||
"marker {:?} overlaps with expected sanitized output {:?}",
|
||||
marker, sanitized
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
violations.is_empty(),
|
||||
"Marker/sanitizer overlap violation(s):\n{}",
|
||||
violations.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_marker_is_substring_of_redactor_patterns() {
|
||||
let markers = oracle_markers();
|
||||
|
||||
let mut violations: Vec<String> = Vec::new();
|
||||
|
||||
for (_, marker, _) in &markers {
|
||||
for pattern in REDACTOR_PREFIXES {
|
||||
// Check if the redactor pattern is a substring of the marker or vice versa.
|
||||
if marker.contains(pattern) && pattern.len() > 3 {
|
||||
violations.push(format!(
|
||||
"marker {:?} contains redactor pattern {:?}",
|
||||
marker, pattern
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
violations.is_empty(),
|
||||
"Marker/redactor overlap violation(s):\n{}",
|
||||
violations.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markers_are_unique_across_caps() {
|
||||
// Per §4.1: a marker may be reused within a single cap (e.g. two SQLi
|
||||
// payloads sharing one oracle marker), but must NOT appear in more than
|
||||
// one cap — that would risk one cap's payload accidentally firing
|
||||
// another cap's oracle.
|
||||
let markers = oracle_markers();
|
||||
|
||||
// Cap is bitflags and does not implement Hash; key by bits().
|
||||
let mut seen: std::collections::HashMap<&str, std::collections::HashSet<u32>> =
|
||||
std::collections::HashMap::new();
|
||||
for (cap, marker, _) in &markers {
|
||||
seen.entry(marker).or_default().insert(cap.bits());
|
||||
}
|
||||
|
||||
let cross_cap: Vec<_> = seen
|
||||
.iter()
|
||||
.filter(|(_, caps)| caps.len() > 1)
|
||||
.map(|(m, caps)| (*m, caps.clone()))
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
cross_cap.is_empty(),
|
||||
"Oracle marker(s) reused across caps (collision risk): {:?}\n\
|
||||
Each cap must use a marker that does not appear in any other cap.",
|
||||
cross_cap
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_vuln_payloads_have_non_empty_oracle_marker() {
|
||||
for cap in [
|
||||
Cap::SQL_QUERY,
|
||||
Cap::CODE_EXEC,
|
||||
Cap::FILE_IO,
|
||||
Cap::SSRF,
|
||||
Cap::HTML_ESCAPE,
|
||||
] {
|
||||
for payload in payloads_for(cap).iter().filter(|p| !p.is_benign) {
|
||||
if let nyx_scanner::dynamic::corpus::Oracle::OutputContains(marker) = payload.oracle {
|
||||
assert!(
|
||||
!marker.is_empty(),
|
||||
"payload {:?} for {cap:?} has empty OutputContains marker",
|
||||
payload.label
|
||||
);
|
||||
assert!(
|
||||
marker.len() >= 4,
|
||||
"payload {:?} for {cap:?} has very short marker {:?} (< 4 chars) — collision risk",
|
||||
payload.label,
|
||||
marker
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue