mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-12 19:55:14 +02:00
[pitboss/grind] deferred session-0002 (20260522T163126Z-7d60)
This commit is contained in:
parent
e4258d63ed
commit
3486056f5e
12 changed files with 1129 additions and 26 deletions
34
tests/dynamic_fixtures/json_parse_depth/go/vuln.go
Normal file
34
tests/dynamic_fixtures/json_parse_depth/go/vuln.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Go JSON_PARSE depth-bomb vuln fixture.
|
||||
//
|
||||
// Models a config-driven JSON ingest endpoint that picks the parser
|
||||
// input based on the request payload tag - `*_DEEP` routes through a
|
||||
// deeply-nested array literal (256 levels) that drives
|
||||
// `encoding/json.Unmarshal` past the 64-level depth budget;
|
||||
// `*_SHALLOW` routes through a flat `[]` parse that leaves the
|
||||
// predicate clear. This shape is needed by the differential runner:
|
||||
// the vuln-payload attempt and the benign-control attempt both load
|
||||
// the same fixture, and only the payload-routed deep branch trips the
|
||||
// `JsonParseExcessiveDepth` predicate.
|
||||
//
|
||||
// Go's encoding/json parser is iterative so the deep input does not
|
||||
// panic the stdlib; the harness walks the returned interface{} to
|
||||
// compute the observed depth and emits a `ProbeKind::JsonParse` record.
|
||||
package vuln
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Run(value string) interface{} {
|
||||
text := value
|
||||
if strings.Contains(text, "DEEP") {
|
||||
nested := strings.Repeat("[", 256) + strings.Repeat("]", 256)
|
||||
var v interface{}
|
||||
_ = json.Unmarshal([]byte(nested), &v)
|
||||
return v
|
||||
}
|
||||
var v interface{}
|
||||
_ = json.Unmarshal([]byte("[]"), &v)
|
||||
return v
|
||||
}
|
||||
37
tests/dynamic_fixtures/json_parse_depth/php/vuln.php
Normal file
37
tests/dynamic_fixtures/json_parse_depth/php/vuln.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
// PHP JSON_PARSE depth-bomb vuln fixture.
|
||||
//
|
||||
// Models a config-driven JSON ingest endpoint that picks the parser
|
||||
// input based on the request payload tag - `*_DEEP` routes through a
|
||||
// deeply-nested array literal (256 levels) that drives `json_decode`
|
||||
// past the 64-level depth budget; `*_SHALLOW` routes through a flat
|
||||
// `[]` parse that leaves the predicate clear. This shape is needed by
|
||||
// the differential runner: the vuln-payload attempt and the
|
||||
// benign-control attempt both load the same fixture, and only the
|
||||
// payload-routed deep branch trips the `JsonParseExcessiveDepth`
|
||||
// predicate.
|
||||
//
|
||||
// PHP cannot monkey-patch `json_decode` itself. The harness publishes
|
||||
// a global `_nyx_json_decode($s)` helper that proxies the real
|
||||
// `json_decode` and records the parse depth before returning. Inside
|
||||
// the synthetic `Nyx\Captured` namespace the harness eval's this
|
||||
// fixture into, PHP's unqualified function-call resolution falls back
|
||||
// to the global namespace, so the call site below routes through the
|
||||
// harness helper at runtime. When this fixture runs standalone (no
|
||||
// harness) the fallback definition near the bottom of the file kicks
|
||||
// in and the helper degrades to a direct `json_decode` call.
|
||||
|
||||
function run($value) {
|
||||
$text = is_string($value) ? $value : (string) json_encode($value);
|
||||
if (strpos($text, 'DEEP') !== false) {
|
||||
$nested = str_repeat('[', 256) . str_repeat(']', 256);
|
||||
return _nyx_json_decode($nested);
|
||||
}
|
||||
return _nyx_json_decode('[]');
|
||||
}
|
||||
|
||||
if (!function_exists('_nyx_json_decode')) {
|
||||
function _nyx_json_decode($s) {
|
||||
return json_decode($s, true, 4096);
|
||||
}
|
||||
}
|
||||
34
tests/dynamic_fixtures/json_parse_depth/rust/vuln.rs
Normal file
34
tests/dynamic_fixtures/json_parse_depth/rust/vuln.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Rust JSON_PARSE depth-bomb vuln fixture.
|
||||
//
|
||||
// Models a config-driven JSON ingest endpoint that picks the parser
|
||||
// input based on the request payload tag - `*_DEEP` routes through a
|
||||
// 100-level nested array literal that drives `serde_json::from_str`
|
||||
// past the 64-level depth budget; `*_SHALLOW` routes through a flat
|
||||
// `[]` parse that leaves the predicate clear. This shape is needed
|
||||
// by the differential runner: the vuln-payload attempt and the
|
||||
// benign-control attempt both load the same fixture, and only the
|
||||
// payload-routed deep branch trips the `JsonParseExcessiveDepth`
|
||||
// predicate.
|
||||
//
|
||||
// `serde_json` defaults to a recursion limit of 128 stack frames
|
||||
// during `from_str`, so the nesting is capped at 100 to stay under
|
||||
// the parser's own guard while still overshooting the predicate's
|
||||
// 64-level budget. The harness walks the returned `Value`
|
||||
// iteratively to compute the observed depth and emits a
|
||||
// `ProbeKind::JsonParse` record.
|
||||
|
||||
pub fn run(value: &str) -> serde_json::Value {
|
||||
if value.contains("DEEP") {
|
||||
let depth = 100usize;
|
||||
let mut nested = String::with_capacity(depth * 2);
|
||||
for _ in 0..depth {
|
||||
nested.push('[');
|
||||
}
|
||||
for _ in 0..depth {
|
||||
nested.push(']');
|
||||
}
|
||||
serde_json::from_str(&nested).unwrap_or(serde_json::Value::Null)
|
||||
} else {
|
||||
serde_json::from_str("[]").unwrap_or(serde_json::Value::Null)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,21 @@ use nyx_scanner::labels::Cap;
|
|||
use nyx_scanner::symbol::Lang;
|
||||
use std::time::Duration;
|
||||
|
||||
const LANGS: &[Lang] = &[Lang::JavaScript, Lang::Python, Lang::Ruby];
|
||||
const LANGS: &[Lang] = &[
|
||||
Lang::JavaScript,
|
||||
Lang::Python,
|
||||
Lang::Ruby,
|
||||
Lang::Php,
|
||||
Lang::Go,
|
||||
Lang::Rust,
|
||||
];
|
||||
|
||||
/// Subset of [`LANGS`] whose JSON parser has a prototype-pollution
|
||||
/// surface — JS / Python / Ruby ship object-property merging idioms
|
||||
/// downstream of `JSON.parse` / `json.loads`. PHP / Go / Rust have no
|
||||
/// equivalent surface so the canary predicate is intentionally absent
|
||||
/// from their corpus slice.
|
||||
const CANARY_LANGS: &[Lang] = &[Lang::JavaScript, Lang::Python, Lang::Ruby];
|
||||
|
||||
fn outcome() -> SandboxOutcome {
|
||||
SandboxOutcome {
|
||||
|
|
@ -61,21 +75,55 @@ fn corpus_registers_json_parse_for_each_supported_lang() {
|
|||
|
||||
#[test]
|
||||
fn json_parse_pairs_benign_per_lang_via_canary_predicate() {
|
||||
for lang in LANGS {
|
||||
for lang in CANARY_LANGS {
|
||||
let slice = payloads_for_lang(Cap::JSON_PARSE, *lang);
|
||||
let vuln = slice.iter().find(|p| !p.is_benign).expect("vuln");
|
||||
let vuln = slice
|
||||
.iter()
|
||||
.find(|p| {
|
||||
!p.is_benign
|
||||
&& matches!(
|
||||
p.oracle,
|
||||
Oracle::SinkProbe {
|
||||
predicates,
|
||||
..
|
||||
} if predicates.iter().any(|q| matches!(
|
||||
q,
|
||||
ProbePredicate::PrototypeCanaryTouched {
|
||||
canary: "__nyx_canary"
|
||||
}
|
||||
))
|
||||
)
|
||||
})
|
||||
.expect("vuln canary payload");
|
||||
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 json_parse_depth_bomb_pairs_benign_per_lang() {
|
||||
for lang in LANGS {
|
||||
let slice = payloads_for_lang(Cap::JSON_PARSE, *lang);
|
||||
let vuln = slice
|
||||
.iter()
|
||||
.find(|p| {
|
||||
!p.is_benign
|
||||
&& matches!(
|
||||
p.oracle,
|
||||
Oracle::SinkProbe {
|
||||
predicates,
|
||||
..
|
||||
} if predicates.iter().any(|q| matches!(
|
||||
q,
|
||||
ProbePredicate::JsonParseExcessiveDepth { max_depth: 64 }
|
||||
))
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| panic!("{lang:?} JSON_PARSE slice must carry a depth-bomb vuln"));
|
||||
let resolved = resolve_benign_control_lang(vuln, Cap::JSON_PARSE, *lang)
|
||||
.expect("depth-bomb benign control resolves");
|
||||
assert!(resolved.is_benign);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +178,10 @@ mod e2e_json_parse_depth {
|
|||
Lang::Python => "python",
|
||||
Lang::JavaScript => "javascript",
|
||||
Lang::Ruby => "ruby",
|
||||
_ => unreachable!("JSON_PARSE depth e2e covers Python + JavaScript + Ruby only"),
|
||||
Lang::Php => "php",
|
||||
Lang::Go => "go",
|
||||
Lang::Rust => "rust",
|
||||
_ => unreachable!("JSON_PARSE depth e2e covers JS / Python / Ruby / PHP / Go / Rust only"),
|
||||
})
|
||||
.join(fixture);
|
||||
let tmp = TempDir::new().expect("create tempdir");
|
||||
|
|
@ -173,7 +224,10 @@ mod e2e_json_parse_depth {
|
|||
Lang::Python => "python3",
|
||||
Lang::JavaScript => "node",
|
||||
Lang::Ruby => "ruby",
|
||||
_ => unreachable!("JSON_PARSE depth e2e covers Python + JavaScript + Ruby only"),
|
||||
Lang::Php => "php",
|
||||
Lang::Go => "go",
|
||||
Lang::Rust => "cargo",
|
||||
_ => unreachable!("JSON_PARSE depth e2e covers JS / Python / Ruby / PHP / Go / Rust only"),
|
||||
};
|
||||
if !command_available(required) {
|
||||
eprintln!("SKIP {lang:?} {fixture}: missing toolchain {required}");
|
||||
|
|
@ -232,19 +286,35 @@ mod e2e_json_parse_depth {
|
|||
};
|
||||
assert_confirmed(Lang::Ruby, &outcome);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_vuln_confirms_via_run_spec() {
|
||||
let Some(outcome) = run(Lang::Php, "vuln.php", "run") else {
|
||||
return;
|
||||
};
|
||||
assert_confirmed(Lang::Php, &outcome);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_vuln_confirms_via_run_spec() {
|
||||
let Some(outcome) = run(Lang::Go, "vuln.go", "Run") else {
|
||||
return;
|
||||
};
|
||||
assert_confirmed(Lang::Go, &outcome);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rust_vuln_confirms_via_run_spec() {
|
||||
let Some(outcome) = run(Lang::Rust, "vuln.rs", "run") else {
|
||||
return;
|
||||
};
|
||||
assert_confirmed(Lang::Rust, &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,
|
||||
] {
|
||||
for lang in [Lang::C, Lang::Cpp, Lang::Java, Lang::TypeScript] {
|
||||
assert!(
|
||||
payloads_for_lang(Cap::JSON_PARSE, lang).is_empty(),
|
||||
"JSON_PARSE has unexpected payloads for {lang:?}",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue