mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0001 (20260522T163126Z-7d60)
This commit is contained in:
parent
fd50549582
commit
e4258d63ed
7 changed files with 625 additions and 13 deletions
|
|
@ -1,17 +1,19 @@
|
|||
//! JavaScript `Cap::JSON_PARSE` payloads — `JSON.parse` then deep
|
||||
//! assign / `Object.assign` chain.
|
||||
//! JavaScript `Cap::JSON_PARSE` payloads.
|
||||
//!
|
||||
//! Same canary oracle as the Phase 10 PROTOTYPE_POLLUTION corpus
|
||||
//! ([`crate::dynamic::oracle::ProbePredicate::PrototypeCanaryTouched`]).
|
||||
//! The harness routes both payloads through `JSON.parse` first to
|
||||
//! exercise the parse-then-assign flow specifically (whereas the
|
||||
//! Phase 10 corpus passes the JSON literal directly to the deep-merge
|
||||
//! sink without an intervening parse).
|
||||
//! Covers two oracle shapes: the prototype-canary pair reuses the
|
||||
//! Phase 10 PROTOTYPE_POLLUTION canary
|
||||
//! ([`crate::dynamic::oracle::ProbePredicate::PrototypeCanaryTouched`])
|
||||
//! against a `JSON.parse`-then-deep-merge fixture, and the depth-bomb
|
||||
//! pair drives `JSON.parse` past the 64-level depth budget for the
|
||||
//! [`crate::dynamic::oracle::ProbePredicate::JsonParseExcessiveDepth`]
|
||||
//! oracle. The depth pair shares a single fixture; the payload tag
|
||||
//! (`NYX_JSON_DEEP` vs `NYX_JSON_SHALLOW`) picks the branch.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
const CANARY: &str = "__nyx_canary";
|
||||
const MAX_DEPTH: u32 = 64;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
|
|
@ -48,4 +50,44 @@ pub const PAYLOADS: &[CuratedPayload] = &[
|
|||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"NYX_JSON_DEEP",
|
||||
label: "json-parse-js-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/javascript/vuln.js"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::JsonParseExcessiveDepth {
|
||||
max_depth: MAX_DEPTH,
|
||||
}],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "json-parse-js-depth-shallow",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"NYX_JSON_SHALLOW",
|
||||
label: "json-parse-js-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/javascript/vuln.js"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
//! Ruby `Cap::JSON_PARSE` payloads — `JSON.parse` then recursive
|
||||
//! `Hash#deep_merge!` on a shared sentinel object.
|
||||
//! Ruby `Cap::JSON_PARSE` payloads.
|
||||
//!
|
||||
//! Covers two oracle shapes: the prototype-canary pair reuses the
|
||||
//! Phase 10 PROTOTYPE_POLLUTION canary against a `JSON.parse` then
|
||||
//! recursive `Hash#deep_merge!` fixture, and the depth-bomb pair
|
||||
//! drives `JSON.parse` past the 64-level depth budget for the
|
||||
//! [`crate::dynamic::oracle::ProbePredicate::JsonParseExcessiveDepth`]
|
||||
//! oracle. The depth pair shares a single fixture; the payload tag
|
||||
//! (`NYX_JSON_DEEP` vs `NYX_JSON_SHALLOW`) picks the branch.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
use crate::dynamic::oracle::ProbePredicate;
|
||||
|
||||
const CANARY: &str = "__nyx_canary";
|
||||
const MAX_DEPTH: u32 = 64;
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
|
|
@ -41,4 +49,44 @@ pub const PAYLOADS: &[CuratedPayload] = &[
|
|||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"NYX_JSON_DEEP",
|
||||
label: "json-parse-ruby-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/ruby/vuln.rb"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[ProbePredicate::JsonParseExcessiveDepth {
|
||||
max_depth: MAX_DEPTH,
|
||||
}],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "json-parse-ruby-depth-shallow",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
bytes: b"NYX_JSON_SHALLOW",
|
||||
label: "json-parse-ruby-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/ruby/vuln.rb"],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -629,6 +629,18 @@ pub fn emit(spec: &HarnessSpec, is_typescript: bool) -> Result<HarnessSource, Un
|
|||
return Ok(emit_prototype_pollution_harness(spec));
|
||||
}
|
||||
|
||||
// Phase 11 (Track J.9): JSON_PARSE depth-bomb short-circuit. The
|
||||
// synthetic harness monkey-patches `JSON.parse`, walks the parsed
|
||||
// value iteratively to record maximum nesting depth, emits a
|
||||
// `ProbeKind::JsonParse { depth, excessive_depth }` record, then
|
||||
// routes the payload through the fixture entry. RangeError-style
|
||||
// V8 stack-exhaustion paths emit `JsonParse { depth: 0,
|
||||
// excessive_depth: true }` so the predicate still fires when the
|
||||
// engine rejects the input outright.
|
||||
if spec.expected_cap == crate::labels::Cap::JSON_PARSE {
|
||||
return Ok(emit_json_parse_harness(spec));
|
||||
}
|
||||
|
||||
// Phase 19 (Track M.1): ClassMethod short-circuit. Same shape gap
|
||||
// closer as the Python emitter — instantiate the class via its
|
||||
// zero-arg constructor (falling back to a stubbed-dependency ctor
|
||||
|
|
@ -1957,6 +1969,135 @@ console.log(JSON.stringify({{
|
|||
}
|
||||
}
|
||||
|
||||
/// Phase 11 (Track J.9) — JSON_PARSE depth-bomb harness for Node.
|
||||
///
|
||||
/// Monkey-patches `JSON.parse` with a wrapper that calls the original
|
||||
/// parser, walks the resulting value iteratively (no recursion stack)
|
||||
/// to compute maximum nesting depth, emits a
|
||||
/// `ProbeKind::JsonParse { depth, excessive_depth }` record, then
|
||||
/// returns the parsed value verbatim. A `RangeError` raised by V8 on
|
||||
/// excessively-deep input is caught and converted into a
|
||||
/// `JsonParse { depth: 0, excessive_depth: true }` probe before the
|
||||
/// error is re-thrown — matching the Python harness's
|
||||
/// `RecursionError` handling.
|
||||
///
|
||||
/// Mirrors `crate::dynamic::lang::python::emit_json_parse_harness`.
|
||||
pub fn emit_json_parse_harness(spec: &HarnessSpec) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
let entry_stem = derive_js_entry_stem(&spec.entry_file);
|
||||
let entry_name = if spec.entry_name.is_empty() {
|
||||
"run".to_owned()
|
||||
} else {
|
||||
spec.entry_name.clone()
|
||||
};
|
||||
let body = format!(
|
||||
r#"// Nyx dynamic harness — JSON_PARSE depth checks (Phase 11 / Track J.9).
|
||||
{shim}
|
||||
|
||||
const _NYX_MAX_WALK = 4096;
|
||||
|
||||
function _nyx_count_depth(parsed) {{
|
||||
let maxDepth = 0;
|
||||
const stack = [[parsed, 1]];
|
||||
let visited = 0;
|
||||
while (stack.length > 0) {{
|
||||
const [cur, depth] = stack.pop();
|
||||
visited += 1;
|
||||
if (visited > _NYX_MAX_WALK) break;
|
||||
if (depth > maxDepth) maxDepth = depth;
|
||||
if (cur !== null && typeof cur === 'object') {{
|
||||
if (Array.isArray(cur)) {{
|
||||
for (let i = 0; i < cur.length; i += 1) {{
|
||||
stack.push([cur[i], depth + 1]);
|
||||
}}
|
||||
}} else {{
|
||||
for (const k of Object.keys(cur)) {{
|
||||
stack.push([cur[k], depth + 1]);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
return maxDepth;
|
||||
}}
|
||||
|
||||
function _nyx_json_parse_probe(depth, excessive) {{
|
||||
const p = process.env.NYX_PROBE_PATH;
|
||||
if (!p) return;
|
||||
const rec = {{
|
||||
sink_callee: 'JSON.parse',
|
||||
args: [{{ kind: 'Int', value: depth | 0 }}],
|
||||
captured_at_ns: Number(process.hrtime.bigint()),
|
||||
payload_id: process.env.NYX_PAYLOAD_ID || '',
|
||||
kind: {{
|
||||
kind: 'JsonParse',
|
||||
depth: depth | 0,
|
||||
excessive_depth: !!excessive,
|
||||
}},
|
||||
witness: __nyx_witness('JSON.parse', [depth | 0]),
|
||||
}};
|
||||
try {{
|
||||
require('fs').appendFileSync(p, JSON.stringify(rec) + '\n');
|
||||
}} catch (e) {{
|
||||
// best-effort
|
||||
}}
|
||||
}}
|
||||
|
||||
const _nyx_orig_json_parse = JSON.parse;
|
||||
|
||||
JSON.parse = function _nyx_json_parse_with_depth(text, reviver) {{
|
||||
let parsed;
|
||||
try {{
|
||||
parsed = _nyx_orig_json_parse(text, reviver);
|
||||
}} catch (e) {{
|
||||
// V8 raises `RangeError: Maximum call stack size exceeded` on
|
||||
// deeply-nested input. Emit the excessive-depth probe before
|
||||
// re-raising so the oracle still fires.
|
||||
if (e instanceof RangeError) {{
|
||||
_nyx_json_parse_probe(0, true);
|
||||
}}
|
||||
throw e;
|
||||
}}
|
||||
const depth = _nyx_count_depth(parsed);
|
||||
_nyx_json_parse_probe(depth, depth > 64);
|
||||
return parsed;
|
||||
}};
|
||||
|
||||
function _nyx_json_parse_via_fixture(payload) {{
|
||||
let _entry;
|
||||
try {{
|
||||
_entry = require('./{entry_stem}');
|
||||
}} catch (e) {{
|
||||
process.stderr.write('NYX_IMPORT_ERROR: ' + e.message + '\n');
|
||||
process.exit(77);
|
||||
}}
|
||||
const fn =
|
||||
_entry && (typeof _entry === 'function' ? _entry : _entry['{entry_name}']);
|
||||
if (typeof fn !== 'function') {{
|
||||
return false;
|
||||
}}
|
||||
try {{
|
||||
fn(payload);
|
||||
}} catch (e) {{
|
||||
// Parser errors / depth-induced throws are expected on the vuln
|
||||
// payload; the probe is already emitted.
|
||||
}}
|
||||
return true;
|
||||
}}
|
||||
|
||||
const payload = process.env.NYX_PAYLOAD || '';
|
||||
_nyx_json_parse_via_fixture(payload);
|
||||
console.log('__NYX_SINK_HIT__');
|
||||
"#
|
||||
);
|
||||
HarnessSource {
|
||||
source: body,
|
||||
filename: "harness.js".to_owned(),
|
||||
command: vec!["node".to_owned(), "harness.js".to_owned()],
|
||||
extra_files: Vec::new(),
|
||||
entry_subpath: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 26 — Node chain-step harness (shared between JS + TS emitters).
|
||||
///
|
||||
/// Splices the Node probe shim ([`probe_shim`]) in front of a minimal
|
||||
|
|
@ -3436,4 +3577,92 @@ mod tests {
|
|||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
fn make_json_parse_spec(entry_file: &str, entry_name: &str) -> HarnessSpec {
|
||||
let mut spec = make_spec(EntryKind::Function, entry_name, PayloadSlot::Param(0));
|
||||
spec.expected_cap = Cap::JSON_PARSE;
|
||||
spec.entry_file = entry_file.into();
|
||||
spec.entry_name = entry_name.into();
|
||||
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/javascript/vuln.js",
|
||||
"run",
|
||||
),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
h.source.contains("_nyx_json_parse_with_depth"),
|
||||
"dispatcher must select the JSON_PARSE depth harness: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("kind: 'JsonParse'"),
|
||||
"JSON_PARSE harness must emit JsonParse probes",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_json_parse_harness_monkey_patches_json_parse() {
|
||||
let h = emit_json_parse_harness(&make_json_parse_spec(
|
||||
"tests/dynamic_fixtures/json_parse_depth/javascript/vuln.js",
|
||||
"run",
|
||||
));
|
||||
assert!(h.source.contains("const _nyx_orig_json_parse = JSON.parse"));
|
||||
assert!(
|
||||
h.source
|
||||
.contains("JSON.parse = function _nyx_json_parse_with_depth")
|
||||
);
|
||||
assert!(h.source.contains("function _nyx_count_depth(parsed)"));
|
||||
}
|
||||
|
||||
#[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/javascript/vuln.js",
|
||||
"run",
|
||||
));
|
||||
assert!(h.source.contains("depth: depth | 0"));
|
||||
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_handles_range_error() {
|
||||
let h = emit_json_parse_harness(&make_json_parse_spec(
|
||||
"tests/dynamic_fixtures/json_parse_depth/javascript/vuln.js",
|
||||
"run",
|
||||
));
|
||||
assert!(h.source.contains("e instanceof RangeError"));
|
||||
assert!(h.source.contains("_nyx_json_parse_probe(0, true)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_json_parse_harness_routes_through_fixture_require() {
|
||||
let h = emit_json_parse_harness(&make_json_parse_spec(
|
||||
"tests/dynamic_fixtures/json_parse_depth/javascript/vuln.js",
|
||||
"run",
|
||||
));
|
||||
assert!(
|
||||
h.source
|
||||
.contains("function _nyx_json_parse_via_fixture(payload)")
|
||||
);
|
||||
assert!(h.source.contains("require('./vuln')"));
|
||||
assert!(h.source.contains("_entry['run']"));
|
||||
assert_eq!(h.filename, "harness.js");
|
||||
assert!(h.extra_files.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_json_parse_harness_derives_entry_stem_from_entry_file() {
|
||||
let h =
|
||||
emit_json_parse_harness(&make_json_parse_spec("/abs/path/benign.js", "run"));
|
||||
assert!(h.source.contains("require('./benign')"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -447,6 +447,18 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
return Ok(emit_open_redirect_harness(spec));
|
||||
}
|
||||
|
||||
// Phase 11 (Track J.9): JSON_PARSE depth-bomb short-circuit. The
|
||||
// synthetic harness rebinds `JSON.parse` to a depth-counting
|
||||
// wrapper, walks the parsed value iteratively, and emits a
|
||||
// `ProbeKind::JsonParse { depth, excessive_depth }` record before
|
||||
// returning the parsed value. `JSON::NestingError` (raised by the
|
||||
// Ruby json gem when the input exceeds `max_nesting`) is caught
|
||||
// and converted into a `JsonParse { depth: 0, excessive_depth:
|
||||
// true }` probe before the error is re-raised.
|
||||
if spec.expected_cap == crate::labels::Cap::JSON_PARSE {
|
||||
return Ok(emit_json_parse_harness(spec));
|
||||
}
|
||||
|
||||
// Phase 19 (Track M.1): ClassMethod short-circuit.
|
||||
if let crate::evidence::EntryKind::ClassMethod { class, method } = &spec.entry_kind {
|
||||
return Ok(emit_class_method_harness(class, method));
|
||||
|
|
@ -1596,6 +1608,131 @@ _nyx_run
|
|||
}
|
||||
}
|
||||
|
||||
/// Phase 11 (Track J.9) — JSON_PARSE depth-bomb harness for Ruby.
|
||||
///
|
||||
/// Rebinds `JSON.parse` to a depth-counting wrapper that calls the
|
||||
/// original parser, walks the resulting value iteratively (no
|
||||
/// recursion stack) to compute maximum nesting depth, emits a
|
||||
/// `ProbeKind::JsonParse { depth, excessive_depth }` record, then
|
||||
/// returns the parsed value verbatim. `JSON::NestingError` raised by
|
||||
/// the Ruby json gem (default `max_nesting` is 100) is caught and
|
||||
/// converted into a `JsonParse { depth: 0, excessive_depth: true }`
|
||||
/// probe before the error is re-raised — matching the Python harness's
|
||||
/// `RecursionError` handling and the JS harness's `RangeError`
|
||||
/// handling.
|
||||
///
|
||||
/// 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_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#"# Nyx dynamic harness — JSON_PARSE depth checks (Phase 11 / Track J.9).
|
||||
require 'json'
|
||||
|
||||
{shim}
|
||||
|
||||
NYX_MAX_WALK = 4096
|
||||
|
||||
def _nyx_count_depth(parsed)
|
||||
max_depth = 0
|
||||
stack = [[parsed, 1]]
|
||||
visited = 0
|
||||
until stack.empty?
|
||||
cur, depth = stack.pop
|
||||
visited += 1
|
||||
break if visited > NYX_MAX_WALK
|
||||
max_depth = depth if depth > max_depth
|
||||
case cur
|
||||
when Hash
|
||||
cur.each_value {{ |v| stack.push([v, depth + 1]) }}
|
||||
when Array
|
||||
cur.each {{ |v| stack.push([v, depth + 1]) }}
|
||||
end
|
||||
end
|
||||
max_depth
|
||||
end
|
||||
|
||||
def _nyx_json_parse_probe(depth, excessive)
|
||||
p = ENV['NYX_PROBE_PATH']
|
||||
return if p.nil? || p.empty?
|
||||
rec = {{
|
||||
'sink_callee' => 'JSON.parse',
|
||||
'args' => [{{ 'kind' => 'Int', 'value' => depth.to_i }}],
|
||||
'captured_at_ns' => Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond),
|
||||
'payload_id' => ENV['NYX_PAYLOAD_ID'] || '',
|
||||
'kind' => {{
|
||||
'kind' => 'JsonParse',
|
||||
'depth' => depth.to_i,
|
||||
'excessive_depth' => !!excessive,
|
||||
}},
|
||||
'witness' => __nyx_witness('JSON.parse', [depth.to_i]),
|
||||
}}
|
||||
begin
|
||||
File.open(p, 'a') {{ |f| f.write(rec.to_json + "\n") }}
|
||||
rescue StandardError
|
||||
# best-effort
|
||||
end
|
||||
end
|
||||
|
||||
_nyx_orig_json_parse = JSON.method(:parse)
|
||||
|
||||
JSON.define_singleton_method(:parse) do |source, *args, **opts|
|
||||
begin
|
||||
parsed = _nyx_orig_json_parse.call(source, *args, **opts)
|
||||
rescue JSON::NestingError => e
|
||||
# The json gem raises NestingError once `max_nesting` (default 100)
|
||||
# is exceeded. Emit the excessive-depth probe before re-raising so
|
||||
# the oracle still fires when the parser rejects the input.
|
||||
_nyx_json_parse_probe(0, true)
|
||||
raise e
|
||||
end
|
||||
depth = _nyx_count_depth(parsed)
|
||||
_nyx_json_parse_probe(depth, depth > 64)
|
||||
parsed
|
||||
end
|
||||
|
||||
def _nyx_json_parse_via_fixture(payload)
|
||||
$LOAD_PATH.unshift('.')
|
||||
begin
|
||||
require_relative './{entry_basename}'
|
||||
rescue LoadError, ScriptError => e
|
||||
STDERR.puts("NYX_IMPORT_ERROR: #{{e.message}}")
|
||||
exit 77
|
||||
end
|
||||
fn_sym = :'{entry_name}'
|
||||
unless Object.respond_to?(fn_sym, true) || self.respond_to?(fn_sym, true)
|
||||
return false
|
||||
end
|
||||
begin
|
||||
send(fn_sym, payload)
|
||||
rescue StandardError
|
||||
# Parser errors / depth-induced raises are expected on the vuln
|
||||
# payload; the probe is already emitted.
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
payload = ENV['NYX_PAYLOAD'] || ''
|
||||
_nyx_json_parse_via_fixture(payload)
|
||||
STDOUT.puts '__NYX_SINK_HIT__'
|
||||
STDOUT.flush
|
||||
"#
|
||||
);
|
||||
HarnessSource {
|
||||
source: body,
|
||||
filename: "harness.rb".to_owned(),
|
||||
command: vec!["ruby".to_owned(), "harness.rb".to_owned()],
|
||||
extra_files: vec![],
|
||||
entry_subpath: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_source(spec: &HarnessSpec, shape: RubyShape) -> String {
|
||||
let entry_fn = &spec.entry_name;
|
||||
let pre_call = build_pre_call(spec);
|
||||
|
|
@ -2434,4 +2571,90 @@ mod tests {
|
|||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
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/ruby/vuln.rb",
|
||||
"run",
|
||||
))
|
||||
.unwrap();
|
||||
assert!(
|
||||
h.source.contains("JSON.define_singleton_method(:parse)"),
|
||||
"dispatcher must select the JSON_PARSE depth harness: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("=> 'JsonParse'"),
|
||||
"JSON_PARSE harness must emit JsonParse probes: {}",
|
||||
h.source
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_json_parse_harness_monkey_patches_json_parse() {
|
||||
let h = emit_json_parse_harness(&make_json_parse_spec(
|
||||
"tests/dynamic_fixtures/json_parse_depth/ruby/vuln.rb",
|
||||
"run",
|
||||
));
|
||||
assert!(h.source.contains("_nyx_orig_json_parse = JSON.method(:parse)"));
|
||||
assert!(
|
||||
h.source.contains("JSON.define_singleton_method(:parse)"),
|
||||
"must rebind JSON.parse: {}",
|
||||
h.source
|
||||
);
|
||||
assert!(h.source.contains("def _nyx_count_depth(parsed)"));
|
||||
}
|
||||
|
||||
#[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/ruby/vuln.rb",
|
||||
"run",
|
||||
));
|
||||
assert!(h.source.contains("'depth' => depth.to_i"));
|
||||
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_handles_nesting_error() {
|
||||
let h = emit_json_parse_harness(&make_json_parse_spec(
|
||||
"tests/dynamic_fixtures/json_parse_depth/ruby/vuln.rb",
|
||||
"run",
|
||||
));
|
||||
assert!(h.source.contains("rescue JSON::NestingError => e"));
|
||||
assert!(h.source.contains("_nyx_json_parse_probe(0, true)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_json_parse_harness_routes_through_fixture_require() {
|
||||
let h = emit_json_parse_harness(&make_json_parse_spec(
|
||||
"tests/dynamic_fixtures/json_parse_depth/ruby/vuln.rb",
|
||||
"run",
|
||||
));
|
||||
assert!(
|
||||
h.source
|
||||
.contains("def _nyx_json_parse_via_fixture(payload)")
|
||||
);
|
||||
assert!(h.source.contains("require_relative './vuln'"));
|
||||
assert!(h.source.contains("fn_sym = :'run'"));
|
||||
assert_eq!(h.filename, "harness.rb");
|
||||
assert!(h.extra_files.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_json_parse_harness_derives_entry_basename_from_entry_file() {
|
||||
let h = emit_json_parse_harness(&make_json_parse_spec("/abs/path/benign.rb", "run"));
|
||||
assert!(h.source.contains("require_relative './benign'"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
23
tests/dynamic_fixtures/json_parse_depth/javascript/vuln.js
Normal file
23
tests/dynamic_fixtures/json_parse_depth/javascript/vuln.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// JavaScript 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.parse`
|
||||
// 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.
|
||||
function run(value) {
|
||||
const text = Buffer.isBuffer(value)
|
||||
? value.toString('utf8')
|
||||
: String(value);
|
||||
if (text.indexOf('DEEP') !== -1) {
|
||||
const nested = '['.repeat(256) + ']'.repeat(256);
|
||||
return JSON.parse(nested);
|
||||
}
|
||||
return JSON.parse('[]');
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
23
tests/dynamic_fixtures/json_parse_depth/ruby/vuln.rb
Normal file
23
tests/dynamic_fixtures/json_parse_depth/ruby/vuln.rb
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Ruby 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.parse`
|
||||
# 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. `max_nesting: false` disables the json gem's depth
|
||||
# guard so the harness's depth walker sees the full 256-level shape
|
||||
# rather than triggering `JSON::NestingError` at depth 100.
|
||||
require 'json'
|
||||
|
||||
def run(value)
|
||||
text = value.to_s
|
||||
if text.include?('DEEP')
|
||||
nested = '[' * 256 + ']' * 256
|
||||
return JSON.parse(nested, max_nesting: false)
|
||||
end
|
||||
JSON.parse('[]')
|
||||
end
|
||||
|
|
@ -128,7 +128,9 @@ mod e2e_json_parse_depth {
|
|||
.join("tests/dynamic_fixtures/json_parse_depth")
|
||||
.join(match lang {
|
||||
Lang::Python => "python",
|
||||
_ => unreachable!("JSON_PARSE depth e2e covers Python only"),
|
||||
Lang::JavaScript => "javascript",
|
||||
Lang::Ruby => "ruby",
|
||||
_ => unreachable!("JSON_PARSE depth e2e covers Python + JavaScript + Ruby only"),
|
||||
})
|
||||
.join(fixture);
|
||||
let tmp = TempDir::new().expect("create tempdir");
|
||||
|
|
@ -167,8 +169,14 @@ mod e2e_json_parse_depth {
|
|||
}
|
||||
|
||||
fn run(lang: Lang, fixture: &str, entry_name: &str) -> Option<RunOutcome> {
|
||||
if !command_available("python3") {
|
||||
eprintln!("SKIP {lang:?} {fixture}: missing toolchain python3");
|
||||
let required = match lang {
|
||||
Lang::Python => "python3",
|
||||
Lang::JavaScript => "node",
|
||||
Lang::Ruby => "ruby",
|
||||
_ => unreachable!("JSON_PARSE depth e2e covers Python + JavaScript + Ruby only"),
|
||||
};
|
||||
if !command_available(required) {
|
||||
eprintln!("SKIP {lang:?} {fixture}: missing toolchain {required}");
|
||||
return None;
|
||||
}
|
||||
let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
|
@ -208,6 +216,22 @@ mod e2e_json_parse_depth {
|
|||
};
|
||||
assert_confirmed(Lang::Python, &outcome);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript_vuln_confirms_via_run_spec() {
|
||||
let Some(outcome) = run(Lang::JavaScript, "vuln.js", "run") else {
|
||||
return;
|
||||
};
|
||||
assert_confirmed(Lang::JavaScript, &outcome);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruby_vuln_confirms_via_run_spec() {
|
||||
let Some(outcome) = run(Lang::Ruby, "vuln.rb", "run") else {
|
||||
return;
|
||||
};
|
||||
assert_confirmed(Lang::Ruby, &outcome);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue