mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-30 20:39:39 +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
54
src/dynamic/corpus/json_parse/go.rs
Normal file
54
src/dynamic/corpus/json_parse/go.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
//! Go `Cap::JSON_PARSE` payloads.
|
||||||
|
//!
|
||||||
|
//! The depth pair shares a single fixture; the payload tag
|
||||||
|
//! (`NYX_JSON_DEEP` vs `NYX_JSON_SHALLOW`) picks the branch. Go has
|
||||||
|
//! no prototype-pollution surface so the canary half of the slice is
|
||||||
|
//! intentionally omitted.
|
||||||
|
|
||||||
|
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||||
|
use crate::dynamic::oracle::ProbePredicate;
|
||||||
|
|
||||||
|
const MAX_DEPTH: u32 = 64;
|
||||||
|
|
||||||
|
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||||
|
CuratedPayload {
|
||||||
|
bytes: b"NYX_JSON_DEEP",
|
||||||
|
label: "json-parse-go-depth-bomb",
|
||||||
|
oracle: Oracle::SinkProbe {
|
||||||
|
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
|
||||||
|
max_depth: MAX_DEPTH,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
is_benign: false,
|
||||||
|
provenance: PayloadProvenance::Curated,
|
||||||
|
since_corpus_version: 15,
|
||||||
|
deprecated_at_corpus_version: None,
|
||||||
|
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/go/vuln.go"],
|
||||||
|
oob_nonce_slot: false,
|
||||||
|
probe_predicates: &[ProbePredicate::JsonParseExcessiveDepth {
|
||||||
|
max_depth: MAX_DEPTH,
|
||||||
|
}],
|
||||||
|
benign_control: Some(PayloadRef {
|
||||||
|
label: "json-parse-go-depth-shallow",
|
||||||
|
}),
|
||||||
|
no_benign_control_rationale: None,
|
||||||
|
},
|
||||||
|
CuratedPayload {
|
||||||
|
bytes: b"NYX_JSON_SHALLOW",
|
||||||
|
label: "json-parse-go-depth-shallow",
|
||||||
|
oracle: Oracle::SinkProbe {
|
||||||
|
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
|
||||||
|
max_depth: MAX_DEPTH,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
is_benign: true,
|
||||||
|
provenance: PayloadProvenance::Curated,
|
||||||
|
since_corpus_version: 15,
|
||||||
|
deprecated_at_corpus_version: None,
|
||||||
|
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/go/vuln.go"],
|
||||||
|
oob_nonce_slot: false,
|
||||||
|
probe_predicates: &[],
|
||||||
|
benign_control: None,
|
||||||
|
no_benign_control_rationale: None,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -16,6 +16,9 @@
|
||||||
//! benign control sends a JSON literal whose top-level key is the
|
//! benign control sends a JSON literal whose top-level key is the
|
||||||
//! regular property `data`, leaving the chain untouched.
|
//! regular property `data`, leaving the chain untouched.
|
||||||
|
|
||||||
|
pub mod go;
|
||||||
pub mod javascript;
|
pub mod javascript;
|
||||||
|
pub mod php;
|
||||||
pub mod python;
|
pub mod python;
|
||||||
pub mod ruby;
|
pub mod ruby;
|
||||||
|
pub mod rust;
|
||||||
|
|
|
||||||
54
src/dynamic/corpus/json_parse/php.rs
Normal file
54
src/dynamic/corpus/json_parse/php.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
//! PHP `Cap::JSON_PARSE` payloads.
|
||||||
|
//!
|
||||||
|
//! The depth pair shares a single fixture; the payload tag
|
||||||
|
//! (`NYX_JSON_DEEP` vs `NYX_JSON_SHALLOW`) picks the branch. PHP has
|
||||||
|
//! no prototype-pollution surface so the canary half of the slice is
|
||||||
|
//! intentionally omitted.
|
||||||
|
|
||||||
|
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||||
|
use crate::dynamic::oracle::ProbePredicate;
|
||||||
|
|
||||||
|
const MAX_DEPTH: u32 = 64;
|
||||||
|
|
||||||
|
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||||
|
CuratedPayload {
|
||||||
|
bytes: b"NYX_JSON_DEEP",
|
||||||
|
label: "json-parse-php-depth-bomb",
|
||||||
|
oracle: Oracle::SinkProbe {
|
||||||
|
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
|
||||||
|
max_depth: MAX_DEPTH,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
is_benign: false,
|
||||||
|
provenance: PayloadProvenance::Curated,
|
||||||
|
since_corpus_version: 15,
|
||||||
|
deprecated_at_corpus_version: None,
|
||||||
|
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/php/vuln.php"],
|
||||||
|
oob_nonce_slot: false,
|
||||||
|
probe_predicates: &[ProbePredicate::JsonParseExcessiveDepth {
|
||||||
|
max_depth: MAX_DEPTH,
|
||||||
|
}],
|
||||||
|
benign_control: Some(PayloadRef {
|
||||||
|
label: "json-parse-php-depth-shallow",
|
||||||
|
}),
|
||||||
|
no_benign_control_rationale: None,
|
||||||
|
},
|
||||||
|
CuratedPayload {
|
||||||
|
bytes: b"NYX_JSON_SHALLOW",
|
||||||
|
label: "json-parse-php-depth-shallow",
|
||||||
|
oracle: Oracle::SinkProbe {
|
||||||
|
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
|
||||||
|
max_depth: MAX_DEPTH,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
is_benign: true,
|
||||||
|
provenance: PayloadProvenance::Curated,
|
||||||
|
since_corpus_version: 15,
|
||||||
|
deprecated_at_corpus_version: None,
|
||||||
|
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/php/vuln.php"],
|
||||||
|
oob_nonce_slot: false,
|
||||||
|
probe_predicates: &[],
|
||||||
|
benign_control: None,
|
||||||
|
no_benign_control_rationale: None,
|
||||||
|
},
|
||||||
|
];
|
||||||
54
src/dynamic/corpus/json_parse/rust.rs
Normal file
54
src/dynamic/corpus/json_parse/rust.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
//! Rust `Cap::JSON_PARSE` payloads.
|
||||||
|
//!
|
||||||
|
//! The depth pair shares a single fixture; the payload tag
|
||||||
|
//! (`NYX_JSON_DEEP` vs `NYX_JSON_SHALLOW`) picks the branch. Rust has
|
||||||
|
//! no prototype-pollution surface so the canary half of the slice is
|
||||||
|
//! intentionally omitted.
|
||||||
|
|
||||||
|
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||||
|
use crate::dynamic::oracle::ProbePredicate;
|
||||||
|
|
||||||
|
const MAX_DEPTH: u32 = 64;
|
||||||
|
|
||||||
|
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||||
|
CuratedPayload {
|
||||||
|
bytes: b"NYX_JSON_DEEP",
|
||||||
|
label: "json-parse-rust-depth-bomb",
|
||||||
|
oracle: Oracle::SinkProbe {
|
||||||
|
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
|
||||||
|
max_depth: MAX_DEPTH,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
is_benign: false,
|
||||||
|
provenance: PayloadProvenance::Curated,
|
||||||
|
since_corpus_version: 15,
|
||||||
|
deprecated_at_corpus_version: None,
|
||||||
|
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/rust/vuln.rs"],
|
||||||
|
oob_nonce_slot: false,
|
||||||
|
probe_predicates: &[ProbePredicate::JsonParseExcessiveDepth {
|
||||||
|
max_depth: MAX_DEPTH,
|
||||||
|
}],
|
||||||
|
benign_control: Some(PayloadRef {
|
||||||
|
label: "json-parse-rust-depth-shallow",
|
||||||
|
}),
|
||||||
|
no_benign_control_rationale: None,
|
||||||
|
},
|
||||||
|
CuratedPayload {
|
||||||
|
bytes: b"NYX_JSON_SHALLOW",
|
||||||
|
label: "json-parse-rust-depth-shallow",
|
||||||
|
oracle: Oracle::SinkProbe {
|
||||||
|
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
|
||||||
|
max_depth: MAX_DEPTH,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
is_benign: true,
|
||||||
|
provenance: PayloadProvenance::Curated,
|
||||||
|
since_corpus_version: 15,
|
||||||
|
deprecated_at_corpus_version: None,
|
||||||
|
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/rust/vuln.rs"],
|
||||||
|
oob_nonce_slot: false,
|
||||||
|
probe_predicates: &[],
|
||||||
|
benign_control: None,
|
||||||
|
no_benign_control_rationale: None,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -199,8 +199,11 @@ const ENTRIES: &[(Cap, Lang, &[CuratedPayload])] = &[
|
||||||
Lang::JavaScript,
|
Lang::JavaScript,
|
||||||
json_parse::javascript::PAYLOADS,
|
json_parse::javascript::PAYLOADS,
|
||||||
),
|
),
|
||||||
|
(Cap::JSON_PARSE, Lang::Go, json_parse::go::PAYLOADS),
|
||||||
|
(Cap::JSON_PARSE, Lang::Php, json_parse::php::PAYLOADS),
|
||||||
(Cap::JSON_PARSE, Lang::Python, json_parse::python::PAYLOADS),
|
(Cap::JSON_PARSE, Lang::Python, json_parse::python::PAYLOADS),
|
||||||
(Cap::JSON_PARSE, Lang::Ruby, json_parse::ruby::PAYLOADS),
|
(Cap::JSON_PARSE, Lang::Ruby, json_parse::ruby::PAYLOADS),
|
||||||
|
(Cap::JSON_PARSE, Lang::Rust, json_parse::rust::PAYLOADS),
|
||||||
(
|
(
|
||||||
Cap::UNAUTHORIZED_ID,
|
Cap::UNAUTHORIZED_ID,
|
||||||
Lang::Python,
|
Lang::Python,
|
||||||
|
|
|
||||||
|
|
@ -589,7 +589,21 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||||
return Ok(emit_crypto_harness(spec));
|
return Ok(emit_crypto_harness(spec));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 19 (Track M.1): ClassMethod short-circuit. Go has no
|
// JSON_PARSE depth-bomb short-circuit. The
|
||||||
|
// Go harness imports the fixture under `internal/vulnentry`,
|
||||||
|
// invokes `vulnentry.<EntryFn>(payload)`, then walks the returned
|
||||||
|
// value iteratively and emits a
|
||||||
|
// `ProbeKind::JsonParse { depth, excessive_depth }` probe. The
|
||||||
|
// fixture's `Run` returns the parsed `interface{}` (or `nil` when
|
||||||
|
// `encoding/json.Unmarshal` fails) so the harness can drive the
|
||||||
|
// depth walker without having to intercept the parse call site
|
||||||
|
// itself — Go can't monkey-patch the stdlib parser and a fixture-
|
||||||
|
// side helper would have to be co-located with the entry package.
|
||||||
|
if spec.expected_cap == crate::labels::Cap::JSON_PARSE {
|
||||||
|
return Ok(emit_json_parse_harness(spec));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClassMethod short-circuit. Go has no
|
||||||
// classes — the dispatcher treats `class` as a top-level struct
|
// classes — the dispatcher treats `class` as a top-level struct
|
||||||
// declared in the entry file and `method` as a method on its
|
// declared in the entry file and `method` as a method on its
|
||||||
// value or pointer receiver. The harness instantiates a zero
|
// value or pointer receiver. The harness instantiates a zero
|
||||||
|
|
@ -600,7 +614,7 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||||
return Ok(emit_class_method_harness(class, method));
|
return Ok(emit_class_method_harness(class, method));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 20 (Track M.2): MessageHandler short-circuit. Picks the
|
// MessageHandler short-circuit. Picks the
|
||||||
// broker loopback (Pub/Sub or NATS) by inspecting the spec's
|
// broker loopback (Pub/Sub or NATS) by inspecting the spec's
|
||||||
// framework adapter id and dispatches the payload synchronously to
|
// framework adapter id and dispatches the payload synchronously to
|
||||||
// the named handler function in the entry package.
|
// the named handler function in the entry package.
|
||||||
|
|
@ -608,7 +622,7 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||||
return Ok(emit_message_handler_harness(spec, queue));
|
return Ok(emit_message_handler_harness(spec, queue));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 21 (Track M.3): GraphQLResolver short-circuit (gqlgen).
|
// GraphQLResolver short-circuit (gqlgen).
|
||||||
if let crate::evidence::EntryKind::GraphQLResolver { type_name, field } = &spec.entry_kind {
|
if let crate::evidence::EntryKind::GraphQLResolver { type_name, field } = &spec.entry_kind {
|
||||||
return Ok(emit_graphql_resolver_harness(
|
return Ok(emit_graphql_resolver_harness(
|
||||||
&spec.entry_name,
|
&spec.entry_name,
|
||||||
|
|
@ -1418,6 +1432,144 @@ func nyxWeakKeyProbe(keyInt uint64) {{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Phase 11 (Track J.9) JSON_PARSE depth-bomb harness for Go.
|
||||||
|
///
|
||||||
|
/// Imports the fixture under `internal/vulnentry`, invokes
|
||||||
|
/// `vulnentry.<EntryFn>(payload)`, and walks the returned value
|
||||||
|
/// iteratively to emit a
|
||||||
|
/// [`crate::dynamic::probe::ProbeKind::JsonParse`] probe. The
|
||||||
|
/// fixture's `Run` is expected to call `encoding/json.Unmarshal`
|
||||||
|
/// (which is iterative in the Go stdlib so deeply-nested input never
|
||||||
|
/// panics) and return the parsed `interface{}` so the harness can
|
||||||
|
/// drive the depth walker post-parse. Falls back to a payload-only
|
||||||
|
/// path that emits `JsonParse { depth: 0, excessive_depth: false }`
|
||||||
|
/// when the fixture source is unreachable so the universal sink-hit
|
||||||
|
/// path still fires.
|
||||||
|
pub fn emit_json_parse_harness(spec: &HarnessSpec) -> HarnessSource {
|
||||||
|
let shim = probe_shim();
|
||||||
|
let go_mod = generate_go_mod();
|
||||||
|
let entry_fn = capitalize_first(&spec.entry_name);
|
||||||
|
let entry_source = read_entry_source(&spec.entry_file);
|
||||||
|
let mut extra_files = vec![("go.mod".to_owned(), go_mod)];
|
||||||
|
let tier_a_active = !entry_source.is_empty();
|
||||||
|
let (extra_imports, via_fixture_decl, via_fixture_invoke) = if tier_a_active {
|
||||||
|
let rewritten = rewrite_package(&entry_source, "vulnentry");
|
||||||
|
extra_files.push(("internal/vulnentry/vulnentry.go".to_owned(), rewritten));
|
||||||
|
let decl = format!(
|
||||||
|
r##"const nyxJsonMaxWalk = 4096
|
||||||
|
|
||||||
|
func nyxJsonCountDepth(parsed interface{{}}) int {{
|
||||||
|
type frame struct {{
|
||||||
|
v interface{{}}
|
||||||
|
depth int
|
||||||
|
}}
|
||||||
|
maxDepth := 0
|
||||||
|
stack := []frame{{{{v: parsed, depth: 1}}}}
|
||||||
|
visited := 0
|
||||||
|
for len(stack) > 0 {{
|
||||||
|
f := stack[len(stack)-1]
|
||||||
|
stack = stack[:len(stack)-1]
|
||||||
|
visited++
|
||||||
|
if visited > nyxJsonMaxWalk {{
|
||||||
|
break
|
||||||
|
}}
|
||||||
|
if f.depth > maxDepth {{
|
||||||
|
maxDepth = f.depth
|
||||||
|
}}
|
||||||
|
switch cur := f.v.(type) {{
|
||||||
|
case map[string]interface{{}}:
|
||||||
|
for _, child := range cur {{
|
||||||
|
stack = append(stack, frame{{v: child, depth: f.depth + 1}})
|
||||||
|
}}
|
||||||
|
case []interface{{}}:
|
||||||
|
for _, child := range cur {{
|
||||||
|
stack = append(stack, frame{{v: child, depth: f.depth + 1}})
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
return maxDepth
|
||||||
|
}}
|
||||||
|
|
||||||
|
func nyxJsonParseViaFixture(payload string) (int, bool, bool) {{
|
||||||
|
var depth int
|
||||||
|
var excessive bool
|
||||||
|
var invoked bool
|
||||||
|
defer func() {{ _ = recover() }}()
|
||||||
|
parsed := vulnentry.{entry_fn}(payload)
|
||||||
|
invoked = true
|
||||||
|
depth = nyxJsonCountDepth(parsed)
|
||||||
|
excessive = depth > 64
|
||||||
|
return depth, excessive, invoked
|
||||||
|
}}
|
||||||
|
|
||||||
|
"##
|
||||||
|
);
|
||||||
|
let invoke = "\tdepth, excessive, fixtureInvoked := nyxJsonParseViaFixture(payload)\n\tif !fixtureInvoked {\n\t\tdepth = 0\n\t\texcessive = false\n\t}\n\tnyxJsonParseProbe(depth, excessive)\n".to_owned();
|
||||||
|
(
|
||||||
|
"\n\t\"nyx-harness/internal/vulnentry\"\n",
|
||||||
|
decl,
|
||||||
|
invoke,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
"",
|
||||||
|
String::new(),
|
||||||
|
"\tnyxJsonParseProbe(0, false)\n".to_owned(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let source = format!(
|
||||||
|
r##"// Nyx dynamic harness — JSON_PARSE depth checks (Phase 11 / Track J.9).
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
{extra_imports})
|
||||||
|
|
||||||
|
{shim}
|
||||||
|
|
||||||
|
func nyxJsonParseProbe(depth int, excessive bool) {{
|
||||||
|
__nyx_emit(map[string]interface{{}}{{
|
||||||
|
"sink_callee": "json.Unmarshal",
|
||||||
|
"args": []map[string]interface{{}}{{
|
||||||
|
{{"kind": "Int", "value": depth}},
|
||||||
|
}},
|
||||||
|
"captured_at_ns": uint64(time.Now().UnixNano()),
|
||||||
|
"payload_id": os.Getenv("NYX_PAYLOAD_ID"),
|
||||||
|
"kind": map[string]interface{{}}{{
|
||||||
|
"kind": "JsonParse",
|
||||||
|
"depth": depth,
|
||||||
|
"excessive_depth": excessive,
|
||||||
|
}},
|
||||||
|
"witness": __nyx_witness("json.Unmarshal", []string{{fmt.Sprintf("%d", depth)}}),
|
||||||
|
}})
|
||||||
|
}}
|
||||||
|
|
||||||
|
{via_fixture_decl}func main() {{
|
||||||
|
__nyx_install_crash_guard("json.Unmarshal")
|
||||||
|
defer __nyx_recover_crash("json.Unmarshal")()
|
||||||
|
payload := os.Getenv("NYX_PAYLOAD")
|
||||||
|
{via_fixture_invoke} fmt.Println("__NYX_SINK_HIT__")
|
||||||
|
body, _ := json.Marshal(map[string]interface{{}}{{"payload_len": len(payload)}})
|
||||||
|
fmt.Println(string(body))
|
||||||
|
}}
|
||||||
|
"##
|
||||||
|
);
|
||||||
|
HarnessSource {
|
||||||
|
source,
|
||||||
|
filename: "main.go".to_owned(),
|
||||||
|
command: vec!["./nyx_harness".to_owned()],
|
||||||
|
extra_files,
|
||||||
|
entry_subpath: Some("entry/entry.go".to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Phase 19 (Track M.1) — class-method harness for Go.
|
/// Phase 19 (Track M.1) — class-method harness for Go.
|
||||||
///
|
///
|
||||||
/// `class` is mapped to a struct type declared in `entry/entry.go`
|
/// `class` is mapped to a struct type declared in `entry/entry.go`
|
||||||
|
|
@ -2597,4 +2749,116 @@ mod tests {
|
||||||
"fallback path must still emit a weak-key probe so the universal sink-hit path fires",
|
"fallback path must still emit a weak-key probe so the universal sink-hit path fires",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Phase 11 (Track J.9) Go JSON_PARSE emitter tests ──────────────────────
|
||||||
|
|
||||||
|
fn make_json_parse_spec(entry_file: &str, entry_name: &str) -> HarnessSpec {
|
||||||
|
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||||
|
spec.expected_cap = Cap::JSON_PARSE;
|
||||||
|
spec.entry_file = entry_file.to_owned();
|
||||||
|
spec.entry_name = entry_name.to_owned();
|
||||||
|
spec
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_dispatches_to_json_parse_harness_when_cap_is_json_parse() {
|
||||||
|
let h = emit(&make_json_parse_spec(
|
||||||
|
"tests/dynamic_fixtures/json_parse_depth/go/vuln.go",
|
||||||
|
"Run",
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
h.source.contains("nyxJsonParseProbe"),
|
||||||
|
"dispatcher must short-circuit Cap::JSON_PARSE into emit_json_parse_harness so the depth probe shim is present",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
h.source.contains("\"kind\": \"JsonParse\","),
|
||||||
|
"JSON_PARSE harness must record probes with kind JsonParse",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_json_parse_harness_routes_through_internal_vulnentry_package() {
|
||||||
|
let h = emit_json_parse_harness(&make_json_parse_spec(
|
||||||
|
"tests/dynamic_fixtures/json_parse_depth/go/vuln.go",
|
||||||
|
"Run",
|
||||||
|
));
|
||||||
|
let staged = h
|
||||||
|
.extra_files
|
||||||
|
.iter()
|
||||||
|
.find(|(name, _)| name == "internal/vulnentry/vulnentry.go");
|
||||||
|
assert!(
|
||||||
|
staged.is_some(),
|
||||||
|
"tier-(a) JSON_PARSE harness must stage the fixture under internal/vulnentry/",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
staged.unwrap().1.contains("package vulnentry"),
|
||||||
|
"fixture package name must be rewritten to vulnentry",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
h.source.contains("nyx-harness/internal/vulnentry"),
|
||||||
|
"main.go must import the rewritten vulnentry package",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
h.source.contains("vulnentry.Run(payload)"),
|
||||||
|
"main.go must invoke the entry function on the rewritten fixture",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_json_parse_harness_emits_depth_fields() {
|
||||||
|
let h = emit_json_parse_harness(&make_json_parse_spec(
|
||||||
|
"tests/dynamic_fixtures/json_parse_depth/go/vuln.go",
|
||||||
|
"Run",
|
||||||
|
));
|
||||||
|
assert!(h.source.contains("\"depth\": depth"));
|
||||||
|
assert!(h.source.contains("\"excessive_depth\": excessive"));
|
||||||
|
assert!(h.source.contains("depth > 64"));
|
||||||
|
assert!(h.source.contains("__NYX_SINK_HIT__"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_json_parse_harness_uses_iterative_walker() {
|
||||||
|
let h = emit_json_parse_harness(&make_json_parse_spec(
|
||||||
|
"tests/dynamic_fixtures/json_parse_depth/go/vuln.go",
|
||||||
|
"Run",
|
||||||
|
));
|
||||||
|
assert!(
|
||||||
|
h.source.contains("func nyxJsonCountDepth"),
|
||||||
|
"Go JSON_PARSE harness must define the iterative depth walker",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
h.source.contains("map[string]interface{}:"),
|
||||||
|
"depth walker must dispatch on the JSON object type",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
h.source.contains("[]interface{}:"),
|
||||||
|
"depth walker must dispatch on the JSON array type",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_json_parse_harness_falls_back_when_fixture_source_unavailable() {
|
||||||
|
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||||
|
spec.expected_cap = Cap::JSON_PARSE;
|
||||||
|
spec.entry_file = "/nonexistent/path/missing.go".into();
|
||||||
|
spec.entry_name = "Run".into();
|
||||||
|
let h = emit_json_parse_harness(&spec);
|
||||||
|
let staged = h
|
||||||
|
.extra_files
|
||||||
|
.iter()
|
||||||
|
.find(|(name, _)| name == "internal/vulnentry/vulnentry.go");
|
||||||
|
assert!(
|
||||||
|
staged.is_none(),
|
||||||
|
"fallback path must not stage a vulnentry copy when the fixture cannot be read",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!h.source.contains("nyx-harness/internal/vulnentry"),
|
||||||
|
"fallback path must not import the missing vulnentry package",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
h.source.contains("nyxJsonParseProbe"),
|
||||||
|
"fallback path must still emit a JSON_PARSE probe so the universal sink-hit path fires",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -521,6 +521,17 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||||
return Ok(emit_crypto_harness(spec));
|
return Ok(emit_crypto_harness(spec));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JSON_PARSE depth-bomb short-circuit. PHP
|
||||||
|
// cannot monkey-patch the `json_decode` builtin, so the harness
|
||||||
|
// publishes a global `_nyx_json_decode` helper that the fixture
|
||||||
|
// calls in place of the builtin. Inside the captured namespace
|
||||||
|
// PHP's unqualified function-call resolution falls back to the
|
||||||
|
// global namespace, so a fixture that calls `_nyx_json_decode(...)`
|
||||||
|
// routes through the harness helper without further annotation.
|
||||||
|
if spec.expected_cap == crate::labels::Cap::JSON_PARSE {
|
||||||
|
return Ok(emit_json_parse_harness(spec));
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 19 (Track M.1): ClassMethod short-circuit.
|
// Phase 19 (Track M.1): ClassMethod short-circuit.
|
||||||
if let crate::evidence::EntryKind::ClassMethod { class, method } = &spec.entry_kind {
|
if let crate::evidence::EntryKind::ClassMethod { class, method } = &spec.entry_kind {
|
||||||
return Ok(emit_class_method_harness(class, method));
|
return Ok(emit_class_method_harness(class, method));
|
||||||
|
|
@ -2162,6 +2173,155 @@ _nyx_run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Phase 11 (Track J.9) — JSON_PARSE depth-bomb harness for PHP.
|
||||||
|
///
|
||||||
|
/// The harness publishes a global `_nyx_json_decode($s)` helper that
|
||||||
|
/// proxies the real `json_decode`, runs an iterative depth walker over
|
||||||
|
/// the parsed value, and emits a
|
||||||
|
/// [`crate::dynamic::probe::ProbeKind::JsonParse`] probe record. PHP
|
||||||
|
/// cannot monkey-patch `json_decode` itself, so the per-language fixture
|
||||||
|
/// calls `_nyx_json_decode(...)` instead of the builtin. PHP's
|
||||||
|
/// unqualified function-call resolution inside the synthetic
|
||||||
|
/// `Nyx\Captured` namespace falls back to the global namespace, so the
|
||||||
|
/// fixture call site resolves to the harness helper at runtime.
|
||||||
|
///
|
||||||
|
/// On parser failure with `JSON_ERROR_DEPTH` (which fires when the
|
||||||
|
/// nesting depth exceeds the helper's `$depth` argument) the harness
|
||||||
|
/// emits a `JsonParse { depth: 0, excessive_depth: true }` probe before
|
||||||
|
/// returning `null` — matches the Python `RecursionError` + JS
|
||||||
|
/// `RangeError` excess paths.
|
||||||
|
///
|
||||||
|
/// Mirrors `crate::dynamic::lang::python::emit_json_parse_harness` and
|
||||||
|
/// `crate::dynamic::lang::js_shared::emit_json_parse_harness`.
|
||||||
|
pub fn emit_json_parse_harness(spec: &HarnessSpec) -> HarnessSource {
|
||||||
|
let shim = probe_shim();
|
||||||
|
let entry_basename = derive_php_entry_basename(&spec.entry_file);
|
||||||
|
let entry_name = if spec.entry_name.is_empty() {
|
||||||
|
"run".to_owned()
|
||||||
|
} else {
|
||||||
|
spec.entry_name.clone()
|
||||||
|
};
|
||||||
|
let body = format!(
|
||||||
|
r#"<?php
|
||||||
|
// Nyx dynamic harness — JSON_PARSE depth checks (Phase 11 / Track J.9).
|
||||||
|
{shim}
|
||||||
|
|
||||||
|
const _NYX_JSON_MAX_WALK = 4096;
|
||||||
|
const _NYX_JSON_HELPER_DEPTH = 4096;
|
||||||
|
|
||||||
|
function _nyx_json_count_depth($parsed): int {{
|
||||||
|
$maxDepth = 0;
|
||||||
|
$stack = [[$parsed, 1]];
|
||||||
|
$visited = 0;
|
||||||
|
while (count($stack) > 0) {{
|
||||||
|
[$cur, $depth] = array_pop($stack);
|
||||||
|
$visited += 1;
|
||||||
|
if ($visited > _NYX_JSON_MAX_WALK) {{
|
||||||
|
break;
|
||||||
|
}}
|
||||||
|
if ($depth > $maxDepth) {{
|
||||||
|
$maxDepth = $depth;
|
||||||
|
}}
|
||||||
|
if (is_array($cur)) {{
|
||||||
|
foreach ($cur as $child) {{
|
||||||
|
$stack[] = [$child, $depth + 1];
|
||||||
|
}}
|
||||||
|
}} elseif (is_object($cur)) {{
|
||||||
|
foreach (get_object_vars($cur) as $child) {{
|
||||||
|
$stack[] = [$child, $depth + 1];
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
return $maxDepth;
|
||||||
|
}}
|
||||||
|
|
||||||
|
function _nyx_json_parse_probe(int $depth, bool $excessive): void {{
|
||||||
|
$p = getenv('NYX_PROBE_PATH');
|
||||||
|
if ($p === false || $p === '') return;
|
||||||
|
$rec = [
|
||||||
|
'sink_callee' => 'json_decode',
|
||||||
|
'args' => [['kind' => 'Int', 'value' => $depth]],
|
||||||
|
'captured_at_ns' => (int) hrtime(true),
|
||||||
|
'payload_id' => (string) (getenv('NYX_PAYLOAD_ID') ?: ''),
|
||||||
|
'kind' => [
|
||||||
|
'kind' => 'JsonParse',
|
||||||
|
'depth' => $depth,
|
||||||
|
'excessive_depth' => $excessive,
|
||||||
|
],
|
||||||
|
'witness' => __nyx_witness('json_decode', [(string) $depth]),
|
||||||
|
];
|
||||||
|
@file_put_contents($p, json_encode($rec) . "\n", FILE_APPEND);
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Global helper the fixture calls in place of `json_decode`. Defined
|
||||||
|
// in the global namespace so an unqualified `_nyx_json_decode(...)`
|
||||||
|
// call inside `namespace Nyx\Captured` resolves here.
|
||||||
|
function _nyx_json_decode(string $text, ?bool $assoc = true, int $depth = _NYX_JSON_HELPER_DEPTH, int $flags = 0) {{
|
||||||
|
$parsed = json_decode($text, $assoc, $depth, $flags);
|
||||||
|
if ($parsed === null && json_last_error() !== JSON_ERROR_NONE) {{
|
||||||
|
if (json_last_error() === JSON_ERROR_DEPTH) {{
|
||||||
|
_nyx_json_parse_probe(0, true);
|
||||||
|
}}
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
$observed = _nyx_json_count_depth($parsed);
|
||||||
|
_nyx_json_parse_probe($observed, $observed > 64);
|
||||||
|
return $parsed;
|
||||||
|
}}
|
||||||
|
|
||||||
|
function _nyx_json_parse_via_fixture(string $payload, string $entry_basename, string $entry_name): bool {{
|
||||||
|
$candidate = __DIR__ . DIRECTORY_SEPARATOR . $entry_basename;
|
||||||
|
if (!is_file($candidate)) {{
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
$src = @file_get_contents($candidate);
|
||||||
|
if ($src === false) {{
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
$stripped = preg_replace('/^\s*<\?php\s*/', '', $src);
|
||||||
|
if ($stripped === null) {{
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
$eval_src = "namespace Nyx\\Captured;\n" . $stripped;
|
||||||
|
try {{
|
||||||
|
$eval_result = @eval($eval_src);
|
||||||
|
}} catch (\Throwable $_) {{
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
if ($eval_result === false) {{
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
$fq = 'Nyx\\Captured\\' . $entry_name;
|
||||||
|
if (!function_exists($fq)) {{
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
try {{
|
||||||
|
$fq($payload);
|
||||||
|
}} catch (\Throwable $_) {{
|
||||||
|
// Parser exceptions on the deep payload are expected — the
|
||||||
|
// probe is already emitted before the helper re-raises.
|
||||||
|
}}
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
|
||||||
|
function _nyx_run(): void {{
|
||||||
|
$payload = (string) (getenv('NYX_PAYLOAD') ?: '');
|
||||||
|
_nyx_json_parse_via_fixture($payload, "{entry_basename}", "{entry_name}");
|
||||||
|
echo "__NYX_SINK_HIT__\n";
|
||||||
|
}}
|
||||||
|
|
||||||
|
_nyx_run();
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
HarnessSource {
|
||||||
|
source: body,
|
||||||
|
filename: "harness.php".to_owned(),
|
||||||
|
command: vec!["php".to_owned(), "harness.php".to_owned()],
|
||||||
|
extra_files: Vec::new(),
|
||||||
|
entry_subpath: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Phase 19 (Track M.1) — class-method harness for PHP.
|
/// Phase 19 (Track M.1) — class-method harness for PHP.
|
||||||
///
|
///
|
||||||
/// Includes the entry file, instantiates the class via its default
|
/// Includes the entry file, instantiates the class via its default
|
||||||
|
|
@ -3361,4 +3521,110 @@ mod tests {
|
||||||
h.source
|
h.source
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Phase 11 (Track J.9) PHP JSON_PARSE emitter tests ─────────────────────
|
||||||
|
|
||||||
|
fn make_json_parse_spec(entry_file: &str, entry_name: &str) -> HarnessSpec {
|
||||||
|
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||||
|
spec.expected_cap = Cap::JSON_PARSE;
|
||||||
|
spec.entry_file = entry_file.to_owned();
|
||||||
|
spec.entry_name = entry_name.to_owned();
|
||||||
|
spec
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_dispatches_to_json_parse_harness_when_cap_is_json_parse() {
|
||||||
|
let h = emit(&make_json_parse_spec(
|
||||||
|
"tests/dynamic_fixtures/json_parse_depth/php/vuln.php",
|
||||||
|
"run",
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
h.source.contains("_nyx_json_decode"),
|
||||||
|
"dispatcher must short-circuit Cap::JSON_PARSE into the depth harness: {}",
|
||||||
|
h.source
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
h.source.contains("'kind' => 'JsonParse'"),
|
||||||
|
"JSON_PARSE harness must emit JsonParse probe records",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_json_parse_harness_defines_global_helper() {
|
||||||
|
let h = emit_json_parse_harness(&make_json_parse_spec(
|
||||||
|
"tests/dynamic_fixtures/json_parse_depth/php/vuln.php",
|
||||||
|
"run",
|
||||||
|
));
|
||||||
|
assert!(
|
||||||
|
h.source.contains("function _nyx_json_decode("),
|
||||||
|
"PHP JSON_PARSE harness must publish a global _nyx_json_decode helper the fixture can call",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
h.source.contains("function _nyx_json_count_depth("),
|
||||||
|
"PHP JSON_PARSE harness must define the iterative depth walker",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_json_parse_harness_emits_depth_fields() {
|
||||||
|
let h = emit_json_parse_harness(&make_json_parse_spec(
|
||||||
|
"tests/dynamic_fixtures/json_parse_depth/php/vuln.php",
|
||||||
|
"run",
|
||||||
|
));
|
||||||
|
assert!(h.source.contains("'depth' => $depth"));
|
||||||
|
assert!(h.source.contains("'excessive_depth' => $excessive"));
|
||||||
|
assert!(h.source.contains("$observed > 64"));
|
||||||
|
assert!(h.source.contains("__NYX_SINK_HIT__"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_json_parse_harness_handles_parser_depth_error() {
|
||||||
|
let h = emit_json_parse_harness(&make_json_parse_spec(
|
||||||
|
"tests/dynamic_fixtures/json_parse_depth/php/vuln.php",
|
||||||
|
"run",
|
||||||
|
));
|
||||||
|
assert!(h.source.contains("JSON_ERROR_DEPTH"));
|
||||||
|
assert!(h.source.contains("_nyx_json_parse_probe(0, true)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_json_parse_harness_routes_through_fixture_eval() {
|
||||||
|
let h = emit_json_parse_harness(&make_json_parse_spec(
|
||||||
|
"tests/dynamic_fixtures/json_parse_depth/php/vuln.php",
|
||||||
|
"run",
|
||||||
|
));
|
||||||
|
assert!(
|
||||||
|
h.source.contains("function _nyx_json_parse_via_fixture("),
|
||||||
|
"PHP JSON_PARSE harness must define the fixture-routing helper: {}",
|
||||||
|
h.source
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
h.source.contains("namespace Nyx\\\\Captured"),
|
||||||
|
"PHP JSON_PARSE harness must eval the fixture inside the Nyx\\Captured namespace: {}",
|
||||||
|
h.source
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
h.source.contains("'Nyx\\\\Captured\\\\' . $entry_name"),
|
||||||
|
"PHP JSON_PARSE harness must invoke the fully-qualified namespaced entry: {}",
|
||||||
|
h.source
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
h.source.contains("\"vuln.php\""),
|
||||||
|
"PHP JSON_PARSE harness must pass the entry basename to the helper: {}",
|
||||||
|
h.source
|
||||||
|
);
|
||||||
|
assert_eq!(h.filename, "harness.php");
|
||||||
|
assert!(h.extra_files.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_json_parse_harness_derives_basename_from_entry_file() {
|
||||||
|
let h = emit_json_parse_harness(&make_json_parse_spec("/abs/path/benign.php", "run"));
|
||||||
|
assert!(
|
||||||
|
h.source.contains("\"benign.php\""),
|
||||||
|
"PHP JSON_PARSE harness must use the entry-file basename, not a hard-coded literal: {}",
|
||||||
|
h.source
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1437,6 +1437,122 @@ fn main() {{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Phase 11 — Track J.9 JSON_PARSE depth-bomb harness for Rust.
|
||||||
|
///
|
||||||
|
/// Stages the fixture at `src/entry.rs`, builds against
|
||||||
|
/// `serde_json = "1"` (added to `Cargo.toml` automatically when
|
||||||
|
/// `Cap::JSON_PARSE` is set — see [`generate_cargo_toml_with_extras`]),
|
||||||
|
/// invokes `entry::<entry_name>(&payload)`, walks the returned
|
||||||
|
/// `serde_json::Value` iteratively, and writes a
|
||||||
|
/// `ProbeKind::JsonParse { depth, excessive_depth }` probe.
|
||||||
|
///
|
||||||
|
/// The fixture's entry is expected to return a `serde_json::Value`
|
||||||
|
/// (parsing `&str` / `&[u8]` via `serde_json::from_str` or
|
||||||
|
/// `serde_json::from_slice` and returning the resulting `Value`).
|
||||||
|
/// `serde_json` is iterative so deeply-nested input never panics the
|
||||||
|
/// parser; the harness reads the observed depth off the returned
|
||||||
|
/// value rather than intercepting the parse call site itself.
|
||||||
|
pub fn emit_json_parse_harness(spec: &HarnessSpec) -> HarnessSource {
|
||||||
|
let shim = probe_shim();
|
||||||
|
let entry_fn = if spec.entry_name.is_empty() {
|
||||||
|
"run".to_owned()
|
||||||
|
} else {
|
||||||
|
spec.entry_name.clone()
|
||||||
|
};
|
||||||
|
let cargo_toml = generate_cargo_toml(Cap::JSON_PARSE);
|
||||||
|
|
||||||
|
let main_rs = format!(
|
||||||
|
r##"//! Nyx dynamic harness — JSON_PARSE depth checks (Phase 11 / Track J.9).
|
||||||
|
mod entry;
|
||||||
|
use std::env;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::time::{{SystemTime, UNIX_EPOCH}};
|
||||||
|
|
||||||
|
{shim}
|
||||||
|
|
||||||
|
const NYX_JSON_MAX_WALK: usize = 4096;
|
||||||
|
|
||||||
|
fn nyx_json_count_depth(value: &serde_json::Value) -> u32 {{
|
||||||
|
let mut max_depth: u32 = 0;
|
||||||
|
let mut stack: Vec<(&serde_json::Value, u32)> = Vec::with_capacity(64);
|
||||||
|
stack.push((value, 1));
|
||||||
|
let mut visited: usize = 0;
|
||||||
|
while let Some((cur, depth)) = stack.pop() {{
|
||||||
|
visited += 1;
|
||||||
|
if visited > NYX_JSON_MAX_WALK {{ break; }}
|
||||||
|
if depth > max_depth {{ max_depth = depth; }}
|
||||||
|
match cur {{
|
||||||
|
serde_json::Value::Array(items) => {{
|
||||||
|
for child in items {{
|
||||||
|
stack.push((child, depth + 1));
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
serde_json::Value::Object(map) => {{
|
||||||
|
for child in map.values() {{
|
||||||
|
stack.push((child, depth + 1));
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
_ => {{}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
max_depth
|
||||||
|
}}
|
||||||
|
|
||||||
|
fn nyx_json_parse_probe(depth: u32, excessive: bool) {{
|
||||||
|
let p = match env::var("NYX_PROBE_PATH") {{ Ok(s) => s, Err(_) => return }};
|
||||||
|
if p.is_empty() {{ return; }}
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_nanos() as u64)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let payload_id = env::var("NYX_PAYLOAD_ID").unwrap_or_default();
|
||||||
|
let depth_str = depth.to_string();
|
||||||
|
let excessive_str = if excessive {{ "true" }} else {{ "false" }};
|
||||||
|
let mut line = String::with_capacity(256);
|
||||||
|
line.push_str("{{\"sink_callee\":\"serde_json::from_str\",\"args\":[");
|
||||||
|
line.push_str("{{\"kind\":\"Int\",\"value\":");
|
||||||
|
line.push_str(&depth_str);
|
||||||
|
line.push_str("}}],");
|
||||||
|
line.push_str("\"captured_at_ns\":");
|
||||||
|
line.push_str(&now.to_string());
|
||||||
|
line.push_str(",\"payload_id\":\"");
|
||||||
|
let mut esc_pid = String::new();
|
||||||
|
__nyx_esc(&payload_id, &mut esc_pid);
|
||||||
|
line.push_str(&esc_pid);
|
||||||
|
line.push_str("\",\"kind\":{{\"kind\":\"JsonParse\",\"depth\":");
|
||||||
|
line.push_str(&depth_str);
|
||||||
|
line.push_str(",\"excessive_depth\":");
|
||||||
|
line.push_str(excessive_str);
|
||||||
|
line.push_str("}},\"witness\":");
|
||||||
|
line.push_str(&__nyx_witness_json("serde_json::from_str", &[&depth_str]));
|
||||||
|
line.push_str("}}\n");
|
||||||
|
if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(&p) {{
|
||||||
|
let _ = f.write_all(line.as_bytes());
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
fn main() {{
|
||||||
|
let payload = env::var("NYX_PAYLOAD").unwrap_or_default();
|
||||||
|
__nyx_install_crash_guard("serde_json::from_str");
|
||||||
|
let parsed = entry::{entry_fn}(&payload);
|
||||||
|
let depth = nyx_json_count_depth(&parsed);
|
||||||
|
let excessive = depth > 64;
|
||||||
|
nyx_json_parse_probe(depth, excessive);
|
||||||
|
println!("__NYX_SINK_HIT__");
|
||||||
|
println!("{{{{\"depth\":{{depth}}}}}}", depth = depth);
|
||||||
|
}}
|
||||||
|
"##
|
||||||
|
);
|
||||||
|
HarnessSource {
|
||||||
|
source: main_rs,
|
||||||
|
filename: "src/main.rs".into(),
|
||||||
|
command: vec!["target/release/nyx_harness".into()],
|
||||||
|
extra_files: vec![("Cargo.toml".into(), cargo_toml)],
|
||||||
|
entry_subpath: Some("src/entry.rs".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Emit a Rust harness for `spec`.
|
/// Emit a Rust harness for `spec`.
|
||||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||||
// Phase 08 (Track J.6): HEADER_INJECTION-sink short-circuit. The
|
// Phase 08 (Track J.6): HEADER_INJECTION-sink short-circuit. The
|
||||||
|
|
@ -1469,6 +1585,15 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||||
return Ok(emit_crypto_harness(spec));
|
return Ok(emit_crypto_harness(spec));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 11 (Track J.9): JSON_PARSE depth-bomb short-circuit. Stages
|
||||||
|
// the fixture at `src/entry.rs`, builds against `serde_json = "1"`,
|
||||||
|
// invokes `entry::<entry_name>(&payload)` which is expected to
|
||||||
|
// return a `serde_json::Value`, walks that value iteratively, and
|
||||||
|
// writes a `ProbeKind::JsonParse { depth, excessive_depth }` record.
|
||||||
|
if spec.expected_cap == crate::labels::Cap::JSON_PARSE {
|
||||||
|
return Ok(emit_json_parse_harness(spec));
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 19 (Track M.1): ClassMethod short-circuit. Rust has no
|
// Phase 19 (Track M.1): ClassMethod short-circuit. Rust has no
|
||||||
// class system — the dispatcher maps `class` to a struct exported
|
// class system — the dispatcher maps `class` to a struct exported
|
||||||
// from `entry::`, and `method` to a `&self` method on that
|
// from `entry::`, and `method` to a `&self` method on that
|
||||||
|
|
@ -1835,6 +1960,9 @@ pub fn generate_cargo_toml_with_extras(cap: Cap, needs_percent_encoding: bool) -
|
||||||
if cap.contains(Cap::CRYPTO) {
|
if cap.contains(Cap::CRYPTO) {
|
||||||
deps.push_str("rand = \"0.8\"\n");
|
deps.push_str("rand = \"0.8\"\n");
|
||||||
}
|
}
|
||||||
|
if cap.contains(Cap::JSON_PARSE) {
|
||||||
|
deps.push_str("serde_json = \"1\"\n");
|
||||||
|
}
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"[package]\n\
|
"[package]\n\
|
||||||
|
|
@ -3067,4 +3195,106 @@ mod tests {
|
||||||
h.source
|
h.source
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Phase 11 (Track J.9) Rust JSON_PARSE emitter tests ─────────────────────
|
||||||
|
|
||||||
|
fn make_json_parse_spec(entry_file: &str, entry_name: &str) -> HarnessSpec {
|
||||||
|
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||||
|
spec.expected_cap = Cap::JSON_PARSE;
|
||||||
|
spec.entry_file = entry_file.to_owned();
|
||||||
|
spec.entry_name = entry_name.to_owned();
|
||||||
|
spec
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_dispatches_to_json_parse_harness_when_cap_is_json_parse() {
|
||||||
|
let h = emit(&make_json_parse_spec(
|
||||||
|
"tests/dynamic_fixtures/json_parse_depth/rust/vuln.rs",
|
||||||
|
"run",
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
h.source.contains("fn nyx_json_parse_probe"),
|
||||||
|
"dispatcher must short-circuit Cap::JSON_PARSE into emit_json_parse_harness so the depth probe shim is present: {}",
|
||||||
|
h.source
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
h.source.contains(r#"\"kind\":\"JsonParse\""#),
|
||||||
|
"Rust JSON_PARSE harness must record probes with kind JsonParse: {}",
|
||||||
|
h.source
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_json_parse_harness_invokes_entry_via_mod_entry() {
|
||||||
|
let h = emit_json_parse_harness(&make_json_parse_spec(
|
||||||
|
"tests/dynamic_fixtures/json_parse_depth/rust/vuln.rs",
|
||||||
|
"run",
|
||||||
|
));
|
||||||
|
assert!(
|
||||||
|
h.source.contains("mod entry;"),
|
||||||
|
"Rust JSON_PARSE harness must declare `mod entry;` so the staged fixture is in scope",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
h.source.contains("let parsed = entry::run(&payload);"),
|
||||||
|
"Rust JSON_PARSE harness must invoke the entry function with the payload",
|
||||||
|
);
|
||||||
|
assert_eq!(h.entry_subpath, Some("src/entry.rs".to_string()));
|
||||||
|
assert_eq!(h.filename, "src/main.rs");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_json_parse_harness_cargo_toml_pulls_in_serde_json() {
|
||||||
|
let h = emit_json_parse_harness(&make_json_parse_spec(
|
||||||
|
"tests/dynamic_fixtures/json_parse_depth/rust/vuln.rs",
|
||||||
|
"run",
|
||||||
|
));
|
||||||
|
let cargo = h
|
||||||
|
.extra_files
|
||||||
|
.iter()
|
||||||
|
.find(|(n, _)| n == "Cargo.toml")
|
||||||
|
.expect("Cargo.toml must be in extra_files");
|
||||||
|
assert!(
|
||||||
|
cargo.1.contains("serde_json = \"1\""),
|
||||||
|
"Rust JSON_PARSE harness Cargo.toml must depend on serde_json so the fixture's parser resolves: {}",
|
||||||
|
cargo.1
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
cargo.1.contains("libc = \"0.2\""),
|
||||||
|
"Rust JSON_PARSE harness Cargo.toml must keep libc dep for the probe shim's sigaction path",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_json_parse_harness_uses_iterative_walker() {
|
||||||
|
let h = emit_json_parse_harness(&make_json_parse_spec(
|
||||||
|
"tests/dynamic_fixtures/json_parse_depth/rust/vuln.rs",
|
||||||
|
"run",
|
||||||
|
));
|
||||||
|
assert!(
|
||||||
|
h.source.contains("fn nyx_json_count_depth"),
|
||||||
|
"Rust JSON_PARSE harness must define the iterative depth walker: {}",
|
||||||
|
h.source
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
h.source.contains("serde_json::Value::Array(items)"),
|
||||||
|
"depth walker must dispatch on serde_json::Value::Array",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
h.source.contains("serde_json::Value::Object(map)"),
|
||||||
|
"depth walker must dispatch on serde_json::Value::Object",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_json_parse_harness_emits_depth_fields() {
|
||||||
|
let h = emit_json_parse_harness(&make_json_parse_spec(
|
||||||
|
"tests/dynamic_fixtures/json_parse_depth/rust/vuln.rs",
|
||||||
|
"run",
|
||||||
|
));
|
||||||
|
assert!(h.source.contains(r#"\"depth\":"#));
|
||||||
|
assert!(h.source.contains(r#"\"excessive_depth\":"#));
|
||||||
|
assert!(h.source.contains("depth > 64"));
|
||||||
|
assert!(h.source.contains("__NYX_SINK_HIT__"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 nyx_scanner::symbol::Lang;
|
||||||
use std::time::Duration;
|
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 {
|
fn outcome() -> SandboxOutcome {
|
||||||
SandboxOutcome {
|
SandboxOutcome {
|
||||||
|
|
@ -61,21 +75,55 @@ fn corpus_registers_json_parse_for_each_supported_lang() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn json_parse_pairs_benign_per_lang_via_canary_predicate() {
|
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 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)
|
let resolved = resolve_benign_control_lang(vuln, Cap::JSON_PARSE, *lang)
|
||||||
.expect("benign control resolves");
|
.expect("benign control resolves");
|
||||||
assert!(resolved.is_benign);
|
assert!(resolved.is_benign);
|
||||||
match &vuln.oracle {
|
}
|
||||||
Oracle::SinkProbe { predicates } => assert!(predicates.iter().any(|p| matches!(
|
}
|
||||||
p,
|
|
||||||
ProbePredicate::PrototypeCanaryTouched {
|
#[test]
|
||||||
canary: "__nyx_canary"
|
fn json_parse_depth_bomb_pairs_benign_per_lang() {
|
||||||
}
|
for lang in LANGS {
|
||||||
))),
|
let slice = payloads_for_lang(Cap::JSON_PARSE, *lang);
|
||||||
other => panic!("expected SinkProbe, got {other:?}"),
|
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::Python => "python",
|
||||||
Lang::JavaScript => "javascript",
|
Lang::JavaScript => "javascript",
|
||||||
Lang::Ruby => "ruby",
|
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);
|
.join(fixture);
|
||||||
let tmp = TempDir::new().expect("create tempdir");
|
let tmp = TempDir::new().expect("create tempdir");
|
||||||
|
|
@ -173,7 +224,10 @@ mod e2e_json_parse_depth {
|
||||||
Lang::Python => "python3",
|
Lang::Python => "python3",
|
||||||
Lang::JavaScript => "node",
|
Lang::JavaScript => "node",
|
||||||
Lang::Ruby => "ruby",
|
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) {
|
if !command_available(required) {
|
||||||
eprintln!("SKIP {lang:?} {fixture}: missing toolchain {required}");
|
eprintln!("SKIP {lang:?} {fixture}: missing toolchain {required}");
|
||||||
|
|
@ -232,19 +286,35 @@ mod e2e_json_parse_depth {
|
||||||
};
|
};
|
||||||
assert_confirmed(Lang::Ruby, &outcome);
|
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]
|
#[test]
|
||||||
fn json_parse_unsupported_for_other_langs() {
|
fn json_parse_unsupported_for_other_langs() {
|
||||||
for lang in [
|
for lang in [Lang::C, Lang::Cpp, Lang::Java, Lang::TypeScript] {
|
||||||
Lang::Rust,
|
|
||||||
Lang::C,
|
|
||||||
Lang::Cpp,
|
|
||||||
Lang::Java,
|
|
||||||
Lang::Go,
|
|
||||||
Lang::Php,
|
|
||||||
Lang::TypeScript,
|
|
||||||
] {
|
|
||||||
assert!(
|
assert!(
|
||||||
payloads_for_lang(Cap::JSON_PARSE, lang).is_empty(),
|
payloads_for_lang(Cap::JSON_PARSE, lang).is_empty(),
|
||||||
"JSON_PARSE has unexpected payloads for {lang:?}",
|
"JSON_PARSE has unexpected payloads for {lang:?}",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue