diff --git a/src/dynamic/corpus/json_parse/javascript.rs b/src/dynamic/corpus/json_parse/javascript.rs index 8f4e88be..19a81677 100644 --- a/src/dynamic/corpus/json_parse/javascript.rs +++ b/src/dynamic/corpus/json_parse/javascript.rs @@ -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, + }, ]; diff --git a/src/dynamic/corpus/json_parse/ruby.rs b/src/dynamic/corpus/json_parse/ruby.rs index 5a45fbde..ada2d017 100644 --- a/src/dynamic/corpus/json_parse/ruby.rs +++ b/src/dynamic/corpus/json_parse/ruby.rs @@ -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, + }, ]; diff --git a/src/dynamic/lang/js_shared.rs b/src/dynamic/lang/js_shared.rs index bf8ceefe..6a003aec 100644 --- a/src/dynamic/lang/js_shared.rs +++ b/src/dynamic/lang/js_shared.rs @@ -629,6 +629,18 @@ pub fn emit(spec: &HarnessSpec, is_typescript: bool) -> Result 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')")); + } } diff --git a/src/dynamic/lang/ruby.rs b/src/dynamic/lang/ruby.rs index 3ca73d2b..fc7c173b 100644 --- a/src/dynamic/lang/ruby.rs +++ b/src/dynamic/lang/ruby.rs @@ -447,6 +447,18 @@ pub fn emit(spec: &HarnessSpec) -> Result { 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'")); + } } diff --git a/tests/dynamic_fixtures/json_parse_depth/javascript/vuln.js b/tests/dynamic_fixtures/json_parse_depth/javascript/vuln.js new file mode 100644 index 00000000..d872c392 --- /dev/null +++ b/tests/dynamic_fixtures/json_parse_depth/javascript/vuln.js @@ -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 }; diff --git a/tests/dynamic_fixtures/json_parse_depth/ruby/vuln.rb b/tests/dynamic_fixtures/json_parse_depth/ruby/vuln.rb new file mode 100644 index 00000000..763ed829 --- /dev/null +++ b/tests/dynamic_fixtures/json_parse_depth/ruby/vuln.rb @@ -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 diff --git a/tests/json_parse_corpus.rs b/tests/json_parse_corpus.rs index 29963914..0fecd874 100644 --- a/tests/json_parse_corpus.rs +++ b/tests/json_parse_corpus.rs @@ -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 { - 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]