From 32211079a0995f08a03abd14b4f9dc4aa6be59c7 Mon Sep 17 00:00:00 2001 From: elipeter Date: Fri, 22 May 2026 09:42:18 -0500 Subject: [PATCH] docs(configuration): improve clarity and formatting in configuration documentation --- src/dynamic/corpus/json_parse/python.rs | 49 ++- src/dynamic/framework/adapters/go_routes.rs | 13 +- src/dynamic/framework/adapters/js_nest.rs | 3 +- src/dynamic/framework/adapters/js_routes.rs | 3 +- .../framework/adapters/migration_flyway.rs | 4 +- .../adapters/migration_go_migrate.rs | 5 +- src/dynamic/framework/adapters/php_laravel.rs | 5 +- src/dynamic/framework/adapters/php_routes.rs | 7 +- src/dynamic/framework/adapters/php_symfony.rs | 5 +- src/dynamic/framework/adapters/ruby_hanami.rs | 6 +- src/dynamic/framework/adapters/ruby_routes.rs | 17 +- .../framework/adapters/ruby_sinatra.rs | 8 +- src/dynamic/framework/adapters/rust_actix.rs | 7 +- src/dynamic/framework/adapters/rust_routes.rs | 7 +- src/dynamic/framework/auth_markers.rs | 10 +- src/dynamic/framework/mod.rs | 8 +- src/dynamic/lang/go.rs | 61 ++-- src/dynamic/lang/java.rs | 55 +++- src/dynamic/lang/java_servlet_stubs.rs | 3 +- src/dynamic/lang/js_shared.rs | 78 ++--- src/dynamic/lang/php.rs | 79 ++--- src/dynamic/lang/python.rs | 301 ++++++++++++++---- src/dynamic/lang/ruby.rs | 79 ++--- src/dynamic/lang/rust.rs | 49 ++- src/dynamic/middleware_demotion.rs | 6 +- src/dynamic/oracle.rs | 17 +- src/dynamic/runner.rs | 10 +- src/dynamic/spec.rs | 5 +- src/dynamic/stubs/ldap_server.rs | 11 +- .../json_parse_depth/python/vuln.py | 23 ++ tests/header_injection_corpus.rs | 50 +-- tests/json_parse_corpus.rs | 113 +++++++ 32 files changed, 717 insertions(+), 380 deletions(-) create mode 100644 tests/dynamic_fixtures/json_parse_depth/python/vuln.py diff --git a/src/dynamic/corpus/json_parse/python.rs b/src/dynamic/corpus/json_parse/python.rs index 8816f48c..6ab091d6 100644 --- a/src/dynamic/corpus/json_parse/python.rs +++ b/src/dynamic/corpus/json_parse/python.rs @@ -1,11 +1,14 @@ -//! Python `Cap::JSON_PARSE` payloads — `json.loads` then -//! attribute-pollution via `setattr` / `dict.update` on a shared -//! sentinel object. +//! Python `Cap::JSON_PARSE` payloads. +//! +//! The canary cases cover pollution-style parses. The depth cases drive +//! `json.loads` past the depth oracle while sharing one fixture for the +//! vulnerable and benign attempts. 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 { @@ -42,4 +45,44 @@ pub const PAYLOADS: &[CuratedPayload] = &[ benign_control: None, no_benign_control_rationale: None, }, + CuratedPayload { + bytes: b"NYX_JSON_DEEP", + label: "json-parse-python-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/python/vuln.py"], + oob_nonce_slot: false, + probe_predicates: &[ProbePredicate::JsonParseExcessiveDepth { + max_depth: MAX_DEPTH, + }], + benign_control: Some(PayloadRef { + label: "json-parse-python-depth-shallow", + }), + no_benign_control_rationale: None, + }, + CuratedPayload { + bytes: b"NYX_JSON_SHALLOW", + label: "json-parse-python-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/python/vuln.py"], + oob_nonce_slot: false, + probe_predicates: &[], + benign_control: None, + no_benign_control_rationale: None, + }, ]; diff --git a/src/dynamic/framework/adapters/go_routes.rs b/src/dynamic/framework/adapters/go_routes.rs index bf391906..1c5bbf0f 100644 --- a/src/dynamic/framework/adapters/go_routes.rs +++ b/src/dynamic/framework/adapters/go_routes.rs @@ -277,9 +277,7 @@ pub fn collect_use_middleware(root: Node<'_>, bytes: &[u8]) -> Vec = Vec::new(); for name in raw { - if auth_markers::is_protective(Lang::Go, &name) - && !out.iter().any(|m| m.name == name) - { + if auth_markers::is_protective(Lang::Go, &name) && !out.iter().any(|m| m.name == name) { out.push(MiddlewareShape { name }); } } @@ -540,7 +538,8 @@ mod tests { #[test] fn collect_use_middleware_picks_bare_identifier() { - let src: &[u8] = b"package main\nfunc init() { r := gin.Default(); r.Use(AuthMiddleware) }\n"; + let src: &[u8] = + b"package main\nfunc init() { r := gin.Default(); r.Use(AuthMiddleware) }\n"; let tree = parse(src); let mw = collect_use_middleware(tree.root_node(), src); assert_eq!(mw.len(), 1); @@ -549,8 +548,7 @@ mod tests { #[test] fn collect_use_middleware_picks_selector_marker() { - let src: &[u8] = - b"package main\nfunc init() { e := echo.New(); e.Use(middleware.JWT) }\n"; + let src: &[u8] = b"package main\nfunc init() { e := echo.New(); e.Use(middleware.JWT) }\n"; let tree = parse(src); let mw = collect_use_middleware(tree.root_node(), src); assert_eq!(mw.len(), 1); @@ -597,8 +595,7 @@ mod tests { #[test] fn collect_use_middleware_returns_empty_when_none_recognised() { - let src: &[u8] = - b"package main\nfunc init() { r := gin.Default(); r.GET(\"/x\", Show) }\n"; + let src: &[u8] = b"package main\nfunc init() { r := gin.Default(); r.GET(\"/x\", Show) }\n"; let tree = parse(src); let mw = collect_use_middleware(tree.root_node(), src); assert!(mw.is_empty()); diff --git a/src/dynamic/framework/adapters/js_nest.rs b/src/dynamic/framework/adapters/js_nest.rs index 93bc6699..04dff2f3 100644 --- a/src/dynamic/framework/adapters/js_nest.rs +++ b/src/dynamic/framework/adapters/js_nest.rs @@ -680,7 +680,8 @@ mod tests { #[test] fn records_class_and_method_use_decorators_in_order() { - let src: &[u8] = b"import { Controller, Post, UseGuards, UseInterceptors } from '@nestjs/common';\n\ + let src: &[u8] = + b"import { Controller, Post, UseGuards, UseInterceptors } from '@nestjs/common';\n\ import { AuthGuard } from './auth.guard';\n\ import { LoggingInterceptor } from './logging.interceptor';\n\ import { RoleGuard } from './role.guard';\n\ diff --git a/src/dynamic/framework/adapters/js_routes.rs b/src/dynamic/framework/adapters/js_routes.rs index 6d5e1752..c7768ab2 100644 --- a/src/dynamic/framework/adapters/js_routes.rs +++ b/src/dynamic/framework/adapters/js_routes.rs @@ -900,8 +900,7 @@ mod tests { #[test] fn extract_middleware_skips_member_expression_path_alias() { - let src: &[u8] = - b"app.post('/save', mw.csrf, mw.auth, controller.save);\n"; + let src: &[u8] = b"app.post('/save', mw.csrf, mw.auth, controller.save);\n"; let tree = parse_js(src); let recv = |n: &str| n == "app"; let mw = extract_route_middleware(tree.root_node(), src, "save", &recv); diff --git a/src/dynamic/framework/adapters/migration_flyway.rs b/src/dynamic/framework/adapters/migration_flyway.rs index a0327443..63ff0257 100644 --- a/src/dynamic/framework/adapters/migration_flyway.rs +++ b/src/dynamic/framework/adapters/migration_flyway.rs @@ -72,9 +72,7 @@ fn extract_version(file_bytes: &[u8]) -> Option { .map(|c| if c == '_' { '.' } else { c }) .collect(); if !normalised.is_empty() - && normalised - .chars() - .all(|c| c.is_ascii_digit() || c == '.') + && normalised.chars().all(|c| c.is_ascii_digit() || c == '.') { return Some(normalised); } diff --git a/src/dynamic/framework/adapters/migration_go_migrate.rs b/src/dynamic/framework/adapters/migration_go_migrate.rs index 2f2b1702..efa08df5 100644 --- a/src/dynamic/framework/adapters/migration_go_migrate.rs +++ b/src/dynamic/framework/adapters/migration_go_migrate.rs @@ -24,10 +24,7 @@ const ADAPTER_NAME: &str = "migration-go-migrate"; fn callee_is_go_migrate(name: &str) -> bool { let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name); - matches!( - last, - "Up" | "Down" | "Steps" | "Migrate" | "Force" | "Drop" - ) + matches!(last, "Up" | "Down" | "Steps" | "Migrate" | "Force" | "Drop") } fn source_imports_go_migrate(file_bytes: &[u8]) -> bool { diff --git a/src/dynamic/framework/adapters/php_laravel.rs b/src/dynamic/framework/adapters/php_laravel.rs index 48f2e447..d010533f 100644 --- a/src/dynamic/framework/adapters/php_laravel.rs +++ b/src/dynamic/framework/adapters/php_laravel.rs @@ -161,10 +161,7 @@ mod tests { .detect(&summary("index"), tree.root_node(), src) .expect("binding"); assert!( - binding - .middleware - .iter() - .any(|m| m.name == "auth:sanctum"), + binding.middleware.iter().any(|m| m.name == "auth:sanctum"), "got {:?}", binding.middleware ); diff --git a/src/dynamic/framework/adapters/php_routes.rs b/src/dynamic/framework/adapters/php_routes.rs index 70b84aa0..011d63e4 100644 --- a/src/dynamic/framework/adapters/php_routes.rs +++ b/src/dynamic/framework/adapters/php_routes.rs @@ -698,9 +698,7 @@ pub fn collect_php_middleware(root: Node<'_>, bytes: &[u8]) -> Vec = Vec::new(); for name in raw { - if auth_markers::is_protective(Lang::Php, &name) - && !out.iter().any(|m| m.name == name) - { + if auth_markers::is_protective(Lang::Php, &name) && !out.iter().any(|m| m.name == name) { out.push(MiddlewareShape { name }); } } @@ -926,8 +924,7 @@ mod tests { #[test] fn collects_array_middleware_arg() { - let src: &[u8] = - b"middleware(['auth', 'verified']);\n"; + let src: &[u8] = b"middleware(['auth', 'verified']);\n"; let tree = parse(src); let mw = collect_php_middleware(tree.root_node(), src); assert!(mw.iter().any(|m| m.name == "auth"), "got {mw:?}"); diff --git a/src/dynamic/framework/adapters/php_symfony.rs b/src/dynamic/framework/adapters/php_symfony.rs index a5c40bcf..4bad094d 100644 --- a/src/dynamic/framework/adapters/php_symfony.rs +++ b/src/dynamic/framework/adapters/php_symfony.rs @@ -170,10 +170,7 @@ mod tests { .detect(&summary("show"), tree.root_node(), src) .expect("binding"); assert!( - binding - .middleware - .iter() - .any(|m| m.name == "#[IsGranted]"), + binding.middleware.iter().any(|m| m.name == "#[IsGranted]"), "got {:?}", binding.middleware ); diff --git a/src/dynamic/framework/adapters/ruby_hanami.rs b/src/dynamic/framework/adapters/ruby_hanami.rs index 9a752c98..8fc241b8 100644 --- a/src/dynamic/framework/adapters/ruby_hanami.rs +++ b/src/dynamic/framework/adapters/ruby_hanami.rs @@ -88,7 +88,11 @@ fn parse_inline_route(file_bytes: &[u8], class_name: &str) -> Option<(HttpMethod None } -fn parse_route_line(line: &str, class_orig: &str, class_snake: &str) -> Option<(HttpMethod, String)> { +fn parse_route_line( + line: &str, + class_orig: &str, + class_snake: &str, +) -> Option<(HttpMethod, String)> { let (verb_tok, after) = line.split_once(char::is_whitespace)?; let method = HttpMethod::from_ident(verb_tok)?; let after = after.trim_start(); diff --git a/src/dynamic/framework/adapters/ruby_routes.rs b/src/dynamic/framework/adapters/ruby_routes.rs index 743759a3..4c9a4671 100644 --- a/src/dynamic/framework/adapters/ruby_routes.rs +++ b/src/dynamic/framework/adapters/ruby_routes.rs @@ -8,7 +8,9 @@ //! helpers here keeps the three adapters terse and lets every //! framework share the same placeholder-binding semantics. -use crate::dynamic::framework::{HttpMethod, MiddlewareShape, ParamBinding, ParamSource, auth_markers}; +use crate::dynamic::framework::{ + HttpMethod, MiddlewareShape, ParamBinding, ParamSource, auth_markers, +}; use crate::symbol::Lang; use tree_sitter::Node; @@ -499,9 +501,7 @@ pub fn collect_ruby_middleware(root: Node<'_>, bytes: &[u8]) -> Vec = Vec::new(); for name in raw { - if auth_markers::is_protective(Lang::Ruby, &name) - && !out.iter().any(|m| m.name == name) - { + if auth_markers::is_protective(Lang::Ruby, &name) && !out.iter().any(|m| m.name == name) { out.push(MiddlewareShape { name }); } } @@ -722,7 +722,8 @@ mod tests { fn collects_rails_protect_from_forgery_self_naming() { // `protect_from_forgery with: :exception` carries no positional // arg — the verb itself must be recognised as the marker. - let src: &[u8] = b"class A < ApplicationController\n protect_from_forgery with: :exception\nend\n"; + let src: &[u8] = + b"class A < ApplicationController\n protect_from_forgery with: :exception\nend\n"; let tree = parse(src); let mw = collect_ruby_middleware(tree.root_node(), src); assert!( @@ -744,8 +745,7 @@ mod tests { #[test] fn collects_sinatra_use_rack_attack_rate_limit() { - let src: &[u8] = - b"require 'sinatra'\nuse Rack::Attack\nget '/x' do\n 'ok'\nend\n"; + let src: &[u8] = b"require 'sinatra'\nuse Rack::Attack\nget '/x' do\n 'ok'\nend\n"; let tree = parse(src); let mw = collect_ruby_middleware(tree.root_node(), src); assert!(mw.iter().any(|m| m.name == "Rack::Attack"), "got {mw:?}"); @@ -762,7 +762,8 @@ mod tests { #[test] fn drops_unknown_marker_names() { - let src: &[u8] = b"class A < ApplicationController\n before_action :do_something_custom\nend\n"; + let src: &[u8] = + b"class A < ApplicationController\n before_action :do_something_custom\nend\n"; let tree = parse(src); let mw = collect_ruby_middleware(tree.root_node(), src); // `do_something_custom` is not in the Ruby auth-markers table. diff --git a/src/dynamic/framework/adapters/ruby_sinatra.rs b/src/dynamic/framework/adapters/ruby_sinatra.rs index 88ed1ad1..6d158728 100644 --- a/src/dynamic/framework/adapters/ruby_sinatra.rs +++ b/src/dynamic/framework/adapters/ruby_sinatra.rs @@ -298,16 +298,14 @@ mod tests { #[test] fn populates_middleware_from_use_rack_attack() { - let src: &[u8] = b"require 'sinatra'\nuse Rack::Attack\nget '/run' do |payload|\n payload\nend\n"; + let src: &[u8] = + b"require 'sinatra'\nuse Rack::Attack\nget '/run' do |payload|\n payload\nend\n"; let tree = parse(src); let binding = RubySinatraAdapter .detect(&summary("run"), tree.root_node(), src) .expect("binding"); assert!( - binding - .middleware - .iter() - .any(|m| m.name == "Rack::Attack"), + binding.middleware.iter().any(|m| m.name == "Rack::Attack"), "expected Rack::Attack marker, got {:?}", binding.middleware ); diff --git a/src/dynamic/framework/adapters/rust_actix.rs b/src/dynamic/framework/adapters/rust_actix.rs index 41a564a3..b70bf7a1 100644 --- a/src/dynamic/framework/adapters/rust_actix.rs +++ b/src/dynamic/framework/adapters/rust_actix.rs @@ -177,7 +177,12 @@ mod tests { let binding = RustActixAdapter .detect(&summary("show"), tree.root_node(), src) .expect("binding"); - assert!(binding.middleware.iter().any(|m| m.name.contains("HttpAuthentication"))); + assert!( + binding + .middleware + .iter() + .any(|m| m.name.contains("HttpAuthentication")) + ); } #[test] diff --git a/src/dynamic/framework/adapters/rust_routes.rs b/src/dynamic/framework/adapters/rust_routes.rs index b4c0cf7c..523ff9cd 100644 --- a/src/dynamic/framework/adapters/rust_routes.rs +++ b/src/dynamic/framework/adapters/rust_routes.rs @@ -318,9 +318,7 @@ pub fn collect_rust_middleware(root: Node<'_>, bytes: &[u8]) -> Vec = Vec::new(); for name in raw { - if auth_markers::is_protective(Lang::Rust, &name) - && !out.iter().any(|m| m.name == name) - { + if auth_markers::is_protective(Lang::Rust, &name) && !out.iter().any(|m| m.name == name) { out.push(MiddlewareShape { name }); } } @@ -1117,7 +1115,8 @@ mod tests { #[test] fn collect_rust_middleware_drops_unknown_names() { - let src: &[u8] = b"use axum::Router;\nfn build() -> Router { Router::new().layer(LoggingLayer) }\n"; + let src: &[u8] = + b"use axum::Router;\nfn build() -> Router { Router::new().layer(LoggingLayer) }\n"; let tree = parse(src); let mw = collect_rust_middleware(tree.root_node(), src); assert!(mw.is_empty(), "LoggingLayer is not a recognised marker"); diff --git a/src/dynamic/framework/auth_markers.rs b/src/dynamic/framework/auth_markers.rs index e49a30b1..407294d0 100644 --- a/src/dynamic/framework/auth_markers.rs +++ b/src/dynamic/framework/auth_markers.rs @@ -128,7 +128,10 @@ const PYTHON_EXACT: &[ExactRow] = &[ ("ValidationMiddleware", AuthMarkerKind::InputValidation), ("pydantic_validate", AuthMarkerKind::InputValidation), ("SecurityMiddleware", AuthMarkerKind::OutputSanitization), - ("XContentTypeOptionsMiddleware", AuthMarkerKind::OutputSanitization), + ( + "XContentTypeOptionsMiddleware", + AuthMarkerKind::OutputSanitization, + ), ("bleach_clean", AuthMarkerKind::OutputSanitization), ("RateLimitMiddleware", AuthMarkerKind::RateLimit), ("ratelimit", AuthMarkerKind::RateLimit), @@ -504,10 +507,7 @@ mod tests { classify(Lang::Go, "JWTAuth"), Some(AuthMarkerKind::Authentication) ); - assert_eq!( - classify(Lang::Go, "csrf.New"), - Some(AuthMarkerKind::Csrf) - ); + assert_eq!(classify(Lang::Go, "csrf.New"), Some(AuthMarkerKind::Csrf)); } #[test] diff --git a/src/dynamic/framework/mod.rs b/src/dynamic/framework/mod.rs index e4cea6a1..cf2cd3da 100644 --- a/src/dynamic/framework/mod.rs +++ b/src/dynamic/framework/mod.rs @@ -450,11 +450,13 @@ mod tests { let adapter = LegacyDetectOnlyAdapter; let no_ssa = adapter.detect_with_context(&summary, None, tree.root_node(), src); - assert_eq!(no_ssa.as_ref().map(|b| b.adapter.as_str()), Some("legacy:handler")); + assert_eq!( + no_ssa.as_ref().map(|b| b.adapter.as_str()), + Some("legacy:handler") + ); let mut ssa = SsaFuncSummary::default(); - ssa.typed_call_receivers - .push((0, "Repository".to_string())); + ssa.typed_call_receivers.push((0, "Repository".to_string())); let with_ssa = adapter.detect_with_context(&summary, Some(&ssa), tree.root_node(), src); // Default impl ignores the SSA summary, so both calls produce // the same binding identity. diff --git a/src/dynamic/lang/go.rs b/src/dynamic/lang/go.rs index 0cfec071..c73c4f7a 100644 --- a/src/dynamic/lang/go.rs +++ b/src/dynamic/lang/go.rs @@ -797,11 +797,9 @@ pub fn emit_header_injection_harness(spec: &HarnessSpec) -> HarnessSource { if tier_a_active { let rewritten = rewrite_package(&entry_source, "vulnentry"); - extra_files.push(( - "internal/vulnentry/vulnentry.go".to_owned(), - rewritten, - )); - extra_imports = "\t\"net/http\"\n\t\"net/http/httptest\"\n\n\t\"nyx-harness/internal/vulnentry\"\n"; + extra_files.push(("internal/vulnentry/vulnentry.go".to_owned(), rewritten)); + extra_imports = + "\t\"net/http\"\n\t\"net/http/httptest\"\n\n\t\"nyx-harness/internal/vulnentry\"\n"; via_fixture_decl = format!( r##"func nyxHeaderViaFixture(payload string) bool {{ defer func() {{ _ = recover() }}() @@ -957,14 +955,8 @@ pub fn emit_open_redirect_harness(spec: &HarnessSpec) -> HarnessSource { "\"github.com/gin-gonic/gin\"", "\"nyx-harness/internal/vulnentry/gin\"", ); - extra_files.push(( - "internal/vulnentry/vulnentry.go".to_owned(), - rewritten, - )); - extra_files.push(( - "internal/vulnentry/gin/gin.go".to_owned(), - gin_stub_pkg(), - )); + extra_files.push(("internal/vulnentry/vulnentry.go".to_owned(), rewritten)); + extra_files.push(("internal/vulnentry/gin/gin.go".to_owned(), gin_stub_pkg())); extra_imports.push_str("\t\"net/http\"\n\t\"net/http/httptest\"\n\n\t\"nyx-harness/internal/vulnentry\"\n\t\"nyx-harness/internal/vulnentry/gin\"\n"); via_fixture_decl.push_str(&format!( r##"func nyxRedirectViaFixture(payload string) (string, bool) {{ @@ -989,11 +981,10 @@ pub fn emit_open_redirect_harness(spec: &HarnessSpec) -> HarnessSource { } else if imports_net_http { // Plain stdlib `http.Redirect(w, r, value, status)` fixture. let rewritten = rewrite_package(&entry_source, "vulnentry"); - extra_files.push(( - "internal/vulnentry/vulnentry.go".to_owned(), - rewritten, - )); - extra_imports.push_str("\t\"net/http\"\n\t\"net/http/httptest\"\n\n\t\"nyx-harness/internal/vulnentry\"\n"); + extra_files.push(("internal/vulnentry/vulnentry.go".to_owned(), rewritten)); + extra_imports.push_str( + "\t\"net/http\"\n\t\"net/http/httptest\"\n\n\t\"nyx-harness/internal/vulnentry\"\n", + ); via_fixture_decl.push_str(&format!( r##"func nyxRedirectViaFixture(payload string) (string, bool) {{ defer func() {{ _ = recover() }}() @@ -1314,10 +1305,7 @@ pub fn emit_crypto_harness(spec: &HarnessSpec) -> HarnessSource { 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, - )); + extra_files.push(("internal/vulnentry/vulnentry.go".to_owned(), rewritten)); let decl = format!( r##"func nyxCryptoViaFixture(payload string) (uint64, bool) {{ defer func() {{ _ = recover() }}() @@ -2306,7 +2294,9 @@ mod tests { "tier-(b) header_injection must not import a fixture package", ); assert!( - harness.source.contains("nyxHeaderProbe(\"Set-Cookie\", payload)"), + harness + .source + .contains("nyxHeaderProbe(\"Set-Cookie\", payload)"), "tier-(b) header_injection must emit synthetic Set-Cookie probe", ); assert!( @@ -2330,7 +2320,9 @@ mod tests { "tier-(a) open_redirect must import the rewritten fixture package", ); assert!( - harness.source.contains("nyx-harness/internal/vulnentry/gin"), + harness + .source + .contains("nyx-harness/internal/vulnentry/gin"), "tier-(a) open_redirect must import the local gin stub", ); assert!( @@ -2373,7 +2365,10 @@ mod tests { "tier-(a) open_redirect must stage the gin stub", ); assert!( - staged_gin.unwrap().1.contains("func (c *Context) Redirect("), + staged_gin + .unwrap() + .1 + .contains("func (c *Context) Redirect("), "staged gin stub must expose Redirect", ); } @@ -2412,19 +2407,27 @@ mod tests { spec.entry_file = "/nonexistent/missing.go".into(); let harness = emit_open_redirect_harness(&spec); assert!( - harness.source.contains("func nyxFollowLocation(location string)"), + harness + .source + .contains("func nyxFollowLocation(location string)"), "OPEN_REDIRECT harness must declare the nyxFollowLocation helper", ); assert!( - harness.source.contains("strings.HasPrefix(location, \"http://127.0.0.1\")"), + harness + .source + .contains("strings.HasPrefix(location, \"http://127.0.0.1\")"), "follower must gate on loopback 127.0.0.1 host prefix", ); assert!( - harness.source.contains("strings.HasPrefix(location, \"http://localhost\")"), + harness + .source + .contains("strings.HasPrefix(location, \"http://localhost\")"), "follower must gate on loopback localhost host prefix", ); assert!( - harness.source.contains("strings.HasPrefix(location, \"http://host-gateway\")"), + harness + .source + .contains("strings.HasPrefix(location, \"http://host-gateway\")"), "follower must gate on loopback host-gateway prefix", ); assert!( diff --git a/src/dynamic/lang/java.rs b/src/dynamic/lang/java.rs index bf7ea8da..d535ab17 100644 --- a/src/dynamic/lang/java.rs +++ b/src/dynamic/lang/java.rs @@ -3733,7 +3733,8 @@ mod tests { "Java LDAP harness must read NYX_LDAP_ENDPOINT to route through the stub", ); assert!( - h.source.contains("javax.naming.directory.InitialDirContext"), + h.source + .contains("javax.naming.directory.InitialDirContext"), "Java LDAP harness must import the JNDI InitialDirContext for the BER round-trip", ); assert!( @@ -3793,7 +3794,9 @@ mod tests { "servlet-importing fixture must trigger stub-file emission", ); assert!( - h.source.contains("HttpServletResponse response = new javax.servlet.http.HttpServletResponse()"), + h.source.contains( + "HttpServletResponse response = new javax.servlet.http.HttpServletResponse()" + ), "Java HEADER_INJECTION harness must instantiate the captured-header response wrapper", ); assert!( @@ -3827,11 +3830,13 @@ mod tests { spec.entry_name = "run".into(); let h = emit_header_injection_harness(&spec); assert!( - h.source.contains("jakarta.servlet.http.HttpServletResponse"), + h.source + .contains("jakarta.servlet.http.HttpServletResponse"), "Java HEADER_INJECTION harness must follow the entry source's servlet namespace", ); assert!( - !h.source.contains("javax.servlet.http.HttpServletResponse response"), + !h.source + .contains("javax.servlet.http.HttpServletResponse response"), "Jakarta entry must not instantiate javax response wrapper", ); let _ = std::fs::remove_dir_all(&dir); @@ -3891,7 +3896,8 @@ mod tests { h.extra_files, ); assert!( - h.source.contains("static byte[] nyxWireFrameViaFixture(String payload)"), + h.source + .contains("static byte[] nyxWireFrameViaFixture(String payload)"), "tier-(b) harness must define the wire-frame helper: {}", h.source ); @@ -3901,7 +3907,8 @@ mod tests { h.source ); assert!( - h.source.contains("getDeclaredMethod(\"setCookieValue\", byte[].class)"), + h.source + .contains("getDeclaredMethod(\"setCookieValue\", byte[].class)"), "tier-(b) harness must install the cookie value via reflection: {}", h.source ); @@ -3911,7 +3918,8 @@ mod tests { h.source ); assert!( - h.source.contains("getDeclaredMethod(\"runOnce\", ServerSocket.class)"), + h.source + .contains("getDeclaredMethod(\"runOnce\", ServerSocket.class)"), "tier-(b) harness must drive runOnce on a worker thread: {}", h.source ); @@ -3921,7 +3929,8 @@ mod tests { h.source ); assert!( - h.source.contains("new Socket(InetAddress.getByName(\"127.0.0.1\"), port)"), + h.source + .contains("new Socket(InetAddress.getByName(\"127.0.0.1\"), port)"), "tier-(b) harness must open a client Socket against the bound port: {}", h.source ); @@ -3941,7 +3950,8 @@ mod tests { h.source ); assert!( - h.source.contains("\"{\\\"wire_frame_len\\\":\" + rawBytes.length"), + h.source + .contains("\"{\\\"wire_frame_len\\\":\" + rawBytes.length"), "tier-(b) harness must emit the wire_frame_len stdout marker: {}", h.source ); @@ -4006,7 +4016,9 @@ mod tests { "servlet-importing fixture must trigger stub-file emission", ); assert!( - h.source.contains("HttpServletResponse response = new javax.servlet.http.HttpServletResponse()"), + h.source.contains( + "HttpServletResponse response = new javax.servlet.http.HttpServletResponse()" + ), "Java OPEN_REDIRECT harness must instantiate the captured-redirect response wrapper", ); assert!( @@ -4064,7 +4076,8 @@ mod tests { spec.entry_name = "run".into(); let h = emit_open_redirect_harness(&spec); assert!( - h.source.contains("static void nyxFollowLocation(String location)"), + h.source + .contains("static void nyxFollowLocation(String location)"), "OPEN_REDIRECT harness must declare the nyxFollowLocation helper", ); assert!( @@ -4102,7 +4115,9 @@ mod tests { spec.entry_name = "run".into(); let h = emit_open_redirect_harness(&spec); assert!( - h.source.contains("nyxRedirectProbe(captured, requestHost);\n nyxFollowLocation(captured);"), + h.source.contains( + "nyxRedirectProbe(captured, requestHost);\n nyxFollowLocation(captured);" + ), "tier-(a) must follow the captured Location: value, not the raw payload", ); let _ = std::fs::remove_dir_all(&dir); @@ -4137,7 +4152,8 @@ mod tests { "tier-(a) harness must reflectively load the fixture entry class", ); assert!( - h.source.contains("getDeclaredMethod(\"run\", String.class)"), + h.source + .contains("getDeclaredMethod(\"run\", String.class)"), "tier-(a) harness must reflectively grab the fixture's run(String) method", ); assert!( @@ -4228,7 +4244,8 @@ mod tests { h.source ); assert!( - h.source.contains("getDeclaredMethod(\"run\", String.class)"), + h.source + .contains("getDeclaredMethod(\"run\", String.class)"), "Java CRYPTO harness must look up the entry method with a single String parameter", ); assert!( @@ -4252,7 +4269,8 @@ mod tests { "run", )); assert!( - h.source.contains("\\\"kind\\\":\\\"WeakKey\\\",\\\"key_int\\\":"), + h.source + .contains("\\\"kind\\\":\\\"WeakKey\\\",\\\"key_int\\\":"), "Java CRYPTO harness must emit ProbeKind::WeakKey records carrying a key_int field so the WeakKeyEntropy predicate fires: {}", h.source ); @@ -4269,7 +4287,8 @@ mod tests { "run", )); assert!( - h.source.contains("ByteBuffer.wrap(buf).order(ByteOrder.BIG_ENDIAN).getLong()"), + h.source + .contains("ByteBuffer.wrap(buf).order(ByteOrder.BIG_ENDIAN).getLong()"), "Java CRYPTO harness must use ByteBuffer.getLong() so a 32-byte CSPRNG key produces a key_int whose magnitude exceeds the 16-bit budget", ); assert!( @@ -4293,7 +4312,9 @@ mod tests { "Java CRYPTO harness must fall back to a payload-derived key_int when reflection fails so the universal sink-hit path still fires", ); assert!( - h.source.contains("ClassNotFoundException | NoSuchMethodException | IllegalAccessException"), + h.source.contains( + "ClassNotFoundException | NoSuchMethodException | IllegalAccessException" + ), "Java CRYPTO harness must catch the reflective lookup exceptions and route to the fallback", ); } diff --git a/src/dynamic/lang/java_servlet_stubs.rs b/src/dynamic/lang/java_servlet_stubs.rs index 9fe5a1b7..e6c0f3fe 100644 --- a/src/dynamic/lang/java_servlet_stubs.rs +++ b/src/dynamic/lang/java_servlet_stubs.rs @@ -497,8 +497,7 @@ mod tests { for pkg in &["javax.servlet.http", "jakarta.servlet.http"] { let resp = http_servlet_response(pkg); assert!( - resp.contains("redirectLocation") - && resp.contains("getRedirectedUrl"), + resp.contains("redirectLocation") && resp.contains("getRedirectedUrl"), "{pkg} HttpServletResponse stub missing redirect-capture wiring", ); } diff --git a/src/dynamic/lang/js_shared.rs b/src/dynamic/lang/js_shared.rs index fe25534f..bf8ceefe 100644 --- a/src/dynamic/lang/js_shared.rs +++ b/src/dynamic/lang/js_shared.rs @@ -3029,7 +3029,8 @@ mod tests { h.source ); assert!( - h.source.contains("if (typeof result.length === 'number') return result.length;"), + h.source + .contains("if (typeof result.length === 'number') return result.length;"), "tier-(a) harness must count nodes via the returned array's .length: {}", h.source ); @@ -3051,9 +3052,7 @@ mod tests { "harness must always stage a package.json with the xmldom dep", ); assert!( - h.extra_files - .iter() - .any(|(p, _)| p == "package-lock.json"), + h.extra_files.iter().any(|(p, _)| p == "package-lock.json"), "harness must always stage a package-lock.json", ); let _ = std::fs::remove_dir_all(&dir); @@ -3090,9 +3089,7 @@ mod tests { h.source ); assert!( - h.extra_files - .iter() - .any(|(p, _)| p == "package.json"), + h.extra_files.iter().any(|(p, _)| p == "package.json"), "harness must always stage a package.json (real-xpath dep is required, no synthetic-only path)", ); let _ = std::fs::remove_dir_all(&dir); @@ -3145,10 +3142,7 @@ mod tests { "const http = require('http');\nfunction run(res, value) { res.setHeader('Set-Cookie', value); }\nmodule.exports = { run };\n", ) .unwrap(); - let h = emit_header_injection_harness(&make_header_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_header_injection_harness(&make_header_spec(entry.to_str().unwrap(), "run")); assert!( h.source.contains("function nyxHeaderViaFixture(payload)"), "tier-(a) harness must define nyxHeaderViaFixture: {}", @@ -3165,7 +3159,8 @@ mod tests { h.source ); assert!( - h.source.contains("captured.push([String(name), String(value)])"), + h.source + .contains("captured.push([String(name), String(value)])"), "tier-(a) harness must record (name, value) pairs verbatim: {}", h.source ); @@ -3188,10 +3183,7 @@ mod tests { "function run(res, value) { res.setHeader('Set-Cookie', value); }\nmodule.exports = { run };\n", ) .unwrap(); - let h = emit_header_injection_harness(&make_header_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_header_injection_harness(&make_header_spec(entry.to_str().unwrap(), "run")); assert!( !h.source.contains("function nyxHeaderViaFixture(payload)"), "fallback path must not emit the tier-(a) helper: {}", @@ -3216,12 +3208,10 @@ mod tests { "const net = require('net');\nlet cookieValue = Buffer.alloc(0);\nfunction setCookieValue(v) { cookieValue = Buffer.from(String(v)); }\nfunction createServer() { return net.createServer((s) => { s.write(Buffer.concat([Buffer.from('HTTP/1.0 200 OK\\r\\nSet-Cookie: '), cookieValue, Buffer.from('\\r\\n\\r\\nok')])); s.end(); }); }\nmodule.exports = { setCookieValue, createServer };\n", ) .unwrap(); - let h = emit_header_injection_harness(&make_header_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_header_injection_harness(&make_header_spec(entry.to_str().unwrap(), "run")); assert!( - h.source.contains("async function nyxWireFrameViaFixture(payload)"), + h.source + .contains("async function nyxWireFrameViaFixture(payload)"), "tier-(b) harness must define the async wire-frame helper: {}", h.source ); @@ -3236,7 +3226,8 @@ mod tests { h.source ); assert!( - h.source.contains("'GET / HTTP/1.0\\r\\nHost: 127.0.0.1\\r\\n\\r\\n'"), + h.source + .contains("'GET / HTTP/1.0\\r\\nHost: 127.0.0.1\\r\\n\\r\\n'"), "tier-(b) harness must issue a raw GET over the client socket: {}", h.source ); @@ -3252,7 +3243,8 @@ mod tests { h.source ); assert!( - h.source.contains("if (hname.toLowerCase() !== 'set-cookie')"), + h.source + .contains("if (hname.toLowerCase() !== 'set-cookie')"), "tier-(b) harness must derive a HeaderEmit probe per Set-Cookie line: {}", h.source ); @@ -3270,10 +3262,7 @@ mod tests { "const http = require('http');\nfunction run(res, value) { res.setHeader('Set-Cookie', value); }\nmodule.exports = { run };\n", ) .unwrap(); - let h = emit_header_injection_harness(&make_header_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_header_injection_harness(&make_header_spec(entry.to_str().unwrap(), "run")); assert!( !h.source.contains("async function nyxWireFrameViaFixture"), "http-only harness must not emit the wire-frame helper: {}", @@ -3303,10 +3292,7 @@ mod tests { "const http = require('http');\nfunction run(res, value) { res.setHeader('Set-Cookie', encodeURIComponent(value)); }\nmodule.exports = { run };\n", ) .unwrap(); - let h = emit_header_injection_harness(&make_header_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_header_injection_harness(&make_header_spec(entry.to_str().unwrap(), "run")); assert!( h.source.contains("require('./benign')"), "tier-(a) harness must require the staged fixture by its file_stem: {}", @@ -3326,10 +3312,7 @@ mod tests { "const express = require('express');\nfunction run(req, res, value) { res.redirect(value); }\nmodule.exports = { run };\n", ) .unwrap(); - let h = emit_open_redirect_harness(&make_redirect_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_open_redirect_harness(&make_redirect_spec(entry.to_str().unwrap(), "run")); assert!( h.source.contains("function nyxRedirectViaFixture(payload)"), "tier-(a) harness must define nyxRedirectViaFixture: {}", @@ -3351,7 +3334,8 @@ mod tests { h.source ); assert!( - h.source.contains("if (String(name).toLowerCase() === 'location')"), + h.source + .contains("if (String(name).toLowerCase() === 'location')"), "tier-(a) harness must also capture setHeader('Location', …) writes: {}", h.source ); @@ -3374,10 +3358,7 @@ mod tests { "function run(req, res, value) { res.redirect(value); }\nmodule.exports = { run };\n", ) .unwrap(); - let h = emit_open_redirect_harness(&make_redirect_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_open_redirect_harness(&make_redirect_spec(entry.to_str().unwrap(), "run")); assert!( !h.source.contains("function nyxRedirectViaFixture(payload)"), "fallback path must not emit the tier-(a) helper: {}", @@ -3402,10 +3383,7 @@ mod tests { "const express = require('express');\nfunction run(req, res, value) { res.redirect(value); }\nmodule.exports = { run };\n", ) .unwrap(); - let h = emit_open_redirect_harness(&make_redirect_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_open_redirect_harness(&make_redirect_spec(entry.to_str().unwrap(), "run")); assert!( h.source.contains("function nyxFollowLocation(location)"), "OPEN_REDIRECT harness must declare the nyxFollowLocation helper: {}", @@ -3424,7 +3402,9 @@ mod tests { h.source ); assert!( - h.source.contains("nyxRedirectProbe(location, requestHost);\n nyxFollowLocation(location);"), + h.source.contains( + "nyxRedirectProbe(location, requestHost);\n nyxFollowLocation(location);" + ), "tier-(a) must follow the captured Location after emitting the probe: {}", h.source ); @@ -3442,17 +3422,15 @@ mod tests { "function run(req, res, value) { res.redirect(value); }\nmodule.exports = { run };\n", ) .unwrap(); - let h = emit_open_redirect_harness(&make_redirect_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_open_redirect_harness(&make_redirect_spec(entry.to_str().unwrap(), "run")); assert!( h.source.contains("function nyxFollowLocation(location)"), "fallback path must still declare nyxFollowLocation: {}", h.source ); assert!( - h.source.contains("nyxRedirectProbe(location, requestHost);\nnyxFollowLocation(location);"), + h.source + .contains("nyxRedirectProbe(location, requestHost);\nnyxFollowLocation(location);"), "fallback path must follow the synthetic location after the probe: {}", h.source ); diff --git a/src/dynamic/lang/php.rs b/src/dynamic/lang/php.rs index d488db2a..790207d4 100644 --- a/src/dynamic/lang/php.rs +++ b/src/dynamic/lang/php.rs @@ -2913,10 +2913,7 @@ mod tests { " 'HeaderWireFrame', 'raw_bytes' => $bytes"), + h.source + .contains("'kind' => 'HeaderWireFrame', 'raw_bytes' => $bytes"), "tier-(b) harness must emit a HeaderWireFrame probe carrying the raw header-block bytes: {}", h.source ); @@ -3090,10 +3081,7 @@ mod tests { " 'WeakKey', 'key_int' => $keyInt]"), + h.source + .contains("['kind' => 'WeakKey', 'key_int' => $keyInt]"), "PHP CRYPTO harness must emit ProbeKind::WeakKey records carrying a key_int field so the WeakKeyEntropy predicate fires: {}", h.source ); @@ -3337,7 +3324,8 @@ mod tests { h.source ); assert!( - h.source.contains("str_pad($head, 8, \"\\0\", STR_PAD_LEFT)"), + h.source + .contains("str_pad($head, 8, \"\\0\", STR_PAD_LEFT)"), "PHP CRYPTO harness must left-zero-pad short slices before unpacking", ); assert!( @@ -3353,7 +3341,8 @@ mod tests { "run", )); assert!( - h.source.contains("if ($produced === null) {\n $produced = $payload;\n }"), + h.source + .contains("if ($produced === null) {\n $produced = $payload;\n }"), "PHP CRYPTO harness must fall back to the payload bytes when the fixture path returns null: {}", h.source ); diff --git a/src/dynamic/lang/python.rs b/src/dynamic/lang/python.rs index 200269f9..4833b754 100644 --- a/src/dynamic/lang/python.rs +++ b/src/dynamic/lang/python.rs @@ -700,6 +700,11 @@ pub fn emit(spec: &HarnessSpec) -> Result { return Ok(emit_crypto_harness(spec)); } + // JSON_PARSE uses a dedicated depth-counting harness. + if spec.expected_cap == crate::labels::Cap::JSON_PARSE { + return Ok(emit_json_parse_harness(spec)); + } + // Phase 19 (Track M.1): ClassMethod short-circuit. When the spec's // entry_kind is the data-bearing `ClassMethod { class, method }` // variant the harness instantiates the class via its default @@ -2437,8 +2442,7 @@ pub fn emit_open_redirect_harness(spec: &HarnessSpec) -> HarnessSource { } else { spec.entry_name.clone() }; - let uses_flask = - entry_source.contains("from flask") || entry_source.contains("import flask"); + let uses_flask = entry_source.contains("from flask") || entry_source.contains("import flask"); let via_fixture = if uses_flask { format!( r#"def _nyx_redirect_via_fixture(payload): @@ -2670,6 +2674,121 @@ def _nyx_run(): sys.stdout.flush() +if __name__ == "__main__": + _nyx_run() +"# + ); + HarnessSource { + source: body, + filename: "harness.py".to_owned(), + command: vec!["python3".to_owned(), "harness.py".to_owned()], + extra_files: Vec::new(), + entry_subpath: None, + } +} + +/// JSON_PARSE depth-bomb harness for Python. +/// +/// The harness wraps `json.loads`, records the maximum nested list / dict +/// depth, then calls the fixture entry with `NYX_PAYLOAD`. It treats parser +/// recursion errors as excessive depth so the oracle sees the failure mode. +pub fn emit_json_parse_harness(spec: &HarnessSpec) -> HarnessSource { + let probe = probe_shim(); + let module_name = derive_module_name(&spec.entry_file); + let entry_name = if spec.entry_name.is_empty() { + "run".to_owned() + } else { + spec.entry_name.clone() + }; + let body = format!( + r#"#!/usr/bin/env python3 +"""Nyx dynamic harness for JSON_PARSE depth checks.""" +import importlib +import json +import os +import sys +import time + +{probe} + +_NYX_MAX_WALK = 4096 + + +def _nyx_count_depth(parsed): + max_depth = 0 + stack = [(parsed, 1)] + visited = 0 + while stack: + cur, depth = stack.pop() + visited += 1 + if visited > _NYX_MAX_WALK: + break + if depth > max_depth: + max_depth = depth + if isinstance(cur, dict): + for value in cur.values(): + stack.append((value, depth + 1)) + elif isinstance(cur, list): + for value in cur: + stack.append((value, depth + 1)) + return max_depth + + +def _nyx_json_parse_probe(depth, excessive): + rec = {{ + "sink_callee": "json.loads", + "args": [{{"kind": "Int", "value": int(depth)}}], + "captured_at_ns": time.time_ns(), + "payload_id": os.environ.get("NYX_PAYLOAD_ID", ""), + "kind": {{ + "kind": "JsonParse", + "depth": int(depth), + "excessive_depth": bool(excessive), + }}, + "witness": __nyx_witness("json.loads", [int(depth)]), + }} + __nyx_emit(rec) + + +_nyx_orig_json_loads = json.loads + + +def _nyx_json_loads_with_depth(s, *args, **kwargs): + try: + parsed = _nyx_orig_json_loads(s, *args, **kwargs) + except RecursionError: + _nyx_json_parse_probe(0, True) + raise + depth = _nyx_count_depth(parsed) + _nyx_json_parse_probe(depth, depth > 64) + return parsed + + +json.loads = _nyx_json_loads_with_depth + + +def _nyx_json_parse_via_fixture(payload): + sys.path.insert(0, ".") + try: + mod = importlib.import_module("{module_name}") + except Exception: + return False + fn = getattr(mod, "{entry_name}", None) + if fn is None: + return False + try: + fn(payload) + except Exception: + return True + return True + + +def _nyx_run(): + payload = os.environ.get("NYX_PAYLOAD", "") + _nyx_json_parse_via_fixture(payload) + print("__NYX_SINK_HIT__", flush=True) + + if __name__ == "__main__": _nyx_run() "# @@ -3781,10 +3900,7 @@ mod tests { def run(value):\n response = Response('ok')\n response.headers['Set-Cookie'] = value\n return response\n", ) .unwrap(); - let h = emit_header_injection_harness(&make_header_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_header_injection_harness(&make_header_spec(entry.to_str().unwrap(), "run")); assert!( h.source.contains("def _nyx_header_via_fixture(payload):"), "tier-(a) harness must define the fixture-routing helper: {}", @@ -3811,14 +3927,16 @@ mod tests { h.source ); assert!( - h.source.contains("captured = _nyx_header_via_fixture(payload)"), + h.source + .contains("captured = _nyx_header_via_fixture(payload)"), "harness main must call the fixture-routing helper first: {}", h.source ); assert!( h.source .contains("_nyx_header_probe(\"Set-Cookie\", payload)") - || h.source.contains("value = payload\n _nyx_header_probe(name, value)"), + || h.source + .contains("value = payload\n _nyx_header_probe(name, value)"), "fallback path must still emit a synthetic probe: {}", h.source ); @@ -3831,15 +3949,8 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let entry = dir.join("vuln.py"); - std::fs::write( - &entry, - "def run(value):\n return value\n", - ) - .unwrap(); - let h = emit_header_injection_harness(&make_header_spec( - entry.to_str().unwrap(), - "run", - )); + std::fs::write(&entry, "def run(value):\n return value\n").unwrap(); + let h = emit_header_injection_harness(&make_header_spec(entry.to_str().unwrap(), "run")); assert!( !h.source.contains("import werkzeug.datastructures"), "fallback path must not import werkzeug: {}", @@ -3851,7 +3962,8 @@ mod tests { h.source ); assert!( - h.source.contains("value = payload\n _nyx_header_probe(name, value)"), + h.source + .contains("value = payload\n _nyx_header_probe(name, value)"), "fallback path must keep the synthetic probe: {}", h.source ); @@ -3870,10 +3982,7 @@ mod tests { def run(v):\n return Response('ok')\n", ) .unwrap(); - let h = emit_header_injection_harness(&make_header_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_header_injection_harness(&make_header_spec(entry.to_str().unwrap(), "run")); assert!( h.source.contains("importlib.import_module(\"benign\")"), "module name must come from the entry-file stem: {}", @@ -3895,17 +4004,16 @@ mod tests { class VulnHandler(BaseHTTPRequestHandler):\n cookie_value = b''\n def do_GET(self):\n self.wfile.write(b'HTTP/1.0 200 OK\\r\\nSet-Cookie: ' + self.__class__.cookie_value + b'\\r\\n\\r\\nok')\n", ) .unwrap(); - let h = emit_header_injection_harness(&make_header_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_header_injection_harness(&make_header_spec(entry.to_str().unwrap(), "run")); assert!( - h.source.contains("def _nyx_wire_frame_via_fixture(payload):"), + h.source + .contains("def _nyx_wire_frame_via_fixture(payload):"), "tier-(b) harness must define the wire-frame helper: {}", h.source ); assert!( - h.source.contains("http.server.HTTPServer((\"127.0.0.1\", 0)"), + h.source + .contains("http.server.HTTPServer((\"127.0.0.1\", 0)"), "tier-(b) harness must boot HTTPServer on loopback ephemeral port: {}", h.source ); @@ -3915,7 +4023,8 @@ mod tests { h.source ); assert!( - h.source.contains("raw_bytes = _nyx_wire_frame_via_fixture(payload)"), + h.source + .contains("raw_bytes = _nyx_wire_frame_via_fixture(payload)"), "harness main must call the wire-frame helper first when raw-socket fixture detected: {}", h.source ); @@ -3948,10 +4057,7 @@ mod tests { def run(value):\n response = Response('ok')\n response.headers['Set-Cookie'] = value\n return response\n", ) .unwrap(); - let h = emit_header_injection_harness(&make_header_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_header_injection_harness(&make_header_spec(entry.to_str().unwrap(), "run")); assert!( !h.source.contains("def _nyx_wire_frame_via_fixture"), "flask-only fixture must not pull in the wire-frame helper: {}", @@ -3984,10 +4090,7 @@ mod tests { "from flask import redirect\ndef run(value):\n return redirect(value)\n", ) .unwrap(); - let h = emit_open_redirect_harness(&make_redirect_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_open_redirect_harness(&make_redirect_spec(entry.to_str().unwrap(), "run")); assert!( h.source.contains("def _nyx_redirect_via_fixture(payload):"), "tier-(a) harness must define the fixture-routing helper: {}", @@ -3999,17 +4102,20 @@ mod tests { h.source ); assert!( - h.source.contains("response.headers.get(\"Location\", \"\")"), + h.source + .contains("response.headers.get(\"Location\", \"\")"), "tier-(a) harness must read the Location header off the returned response: {}", h.source ); assert!( - h.source.contains("captured = _nyx_redirect_via_fixture(payload)"), + h.source + .contains("captured = _nyx_redirect_via_fixture(payload)"), "harness main must call the fixture-routing helper first: {}", h.source ); assert!( - h.source.contains("location = payload\n _nyx_redirect_probe(location, request_host)"), + h.source + .contains("location = payload\n _nyx_redirect_probe(location, request_host)"), "fallback path must keep the synthetic probe: {}", h.source ); @@ -4022,15 +4128,8 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let entry = dir.join("vuln.py"); - std::fs::write( - &entry, - "def run(value):\n return value\n", - ) - .unwrap(); - let h = emit_open_redirect_harness(&make_redirect_spec( - entry.to_str().unwrap(), - "run", - )); + std::fs::write(&entry, "def run(value):\n return value\n").unwrap(); + let h = emit_open_redirect_harness(&make_redirect_spec(entry.to_str().unwrap(), "run")); assert!( !h.source.contains("def _nyx_redirect_via_fixture"), "fallback path must not define the fixture-routing helper: {}", @@ -4042,7 +4141,8 @@ mod tests { h.source ); assert!( - h.source.contains("location = payload\n _nyx_redirect_probe(location, request_host)"), + h.source + .contains("location = payload\n _nyx_redirect_probe(location, request_host)"), "fallback path must keep the synthetic probe: {}", h.source ); @@ -4060,10 +4160,7 @@ mod tests { "from flask import redirect\ndef run(value):\n return redirect(value)\n", ) .unwrap(); - let h = emit_open_redirect_harness(&make_redirect_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_open_redirect_harness(&make_redirect_spec(entry.to_str().unwrap(), "run")); assert!( h.source.contains("def _nyx_follow_location(location):"), "OPEN_REDIRECT harness must declare the _nyx_follow_location helper: {}", @@ -4075,7 +4172,8 @@ mod tests { h.source ); assert!( - h.source.contains("urllib.request.urlopen(location, timeout=2.0)"), + h.source + .contains("urllib.request.urlopen(location, timeout=2.0)"), "follow-location helper must call urllib.request.urlopen with a 2-second timeout: {}", h.source ); @@ -4100,17 +4198,16 @@ mod tests { "from flask import redirect\ndef run(value):\n return redirect(value)\n", ) .unwrap(); - let h = emit_open_redirect_harness(&make_redirect_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_open_redirect_harness(&make_redirect_spec(entry.to_str().unwrap(), "run")); assert!( h.source.contains("_nyx_redirect_probe(location, request_host)\n _nyx_follow_location(location)"), "tier-(a) must follow the captured Location after emitting the probe: {}", h.source ); assert!( - h.source.contains("_nyx_redirect_probe(location, request_host)\n _nyx_follow_location(location)"), + h.source.contains( + "_nyx_redirect_probe(location, request_host)\n _nyx_follow_location(location)" + ), "tier-(b) fallback must also follow the synthetic location after the probe: {}", h.source ); @@ -4162,7 +4259,8 @@ mod tests { "Python CRYPTO harness must look up the entry function by name", ); assert!( - h.source.contains("produced = _nyx_crypto_via_fixture(payload)"), + h.source + .contains("produced = _nyx_crypto_via_fixture(payload)"), "Python CRYPTO harness main must call the fixture-routing helper", ); assert_eq!( @@ -4216,4 +4314,85 @@ mod tests { "module name must come from the entry-file stem, not a hard-coded literal", ); } + + 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/python/vuln.py", + "run", + )) + .unwrap(); + assert!( + h.source.contains("_nyx_json_loads_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_loads() { + let h = emit_json_parse_harness(&make_json_parse_spec( + "tests/dynamic_fixtures/json_parse_depth/python/vuln.py", + "run", + )); + assert!(h.source.contains("_nyx_orig_json_loads = json.loads")); + assert!(h.source.contains("json.loads = _nyx_json_loads_with_depth")); + 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/python/vuln.py", + "run", + )); + assert!(h.source.contains("\"depth\": int(depth)")); + assert!(h.source.contains("\"excessive_depth\": bool(excessive)")); + assert!(h.source.contains("depth > 64")); + assert!(h.source.contains("__NYX_SINK_HIT__")); + } + + #[test] + fn emit_json_parse_harness_handles_parser_recursion_error() { + let h = emit_json_parse_harness(&make_json_parse_spec( + "tests/dynamic_fixtures/json_parse_depth/python/vuln.py", + "run", + )); + assert!(h.source.contains("except RecursionError:")); + assert!(h.source.contains("_nyx_json_parse_probe(0, True)")); + } + + #[test] + fn emit_json_parse_harness_routes_through_fixture_import() { + let h = emit_json_parse_harness(&make_json_parse_spec( + "tests/dynamic_fixtures/json_parse_depth/python/vuln.py", + "run", + )); + assert!( + h.source + .contains("def _nyx_json_parse_via_fixture(payload):") + ); + assert!(h.source.contains("importlib.import_module(\"vuln\")")); + assert!(h.source.contains("getattr(mod, \"run\", None)")); + assert_eq!(h.filename, "harness.py"); + assert!(h.extra_files.is_empty()); + } + + #[test] + fn emit_json_parse_harness_derives_module_name_from_entry_file() { + let h = emit_json_parse_harness(&make_json_parse_spec("/abs/path/benign.py", "run")); + assert!(h.source.contains("importlib.import_module(\"benign\")")); + } } diff --git a/src/dynamic/lang/ruby.rs b/src/dynamic/lang/ruby.rs index 540215e3..3ca73d2b 100644 --- a/src/dynamic/lang/ruby.rs +++ b/src/dynamic/lang/ruby.rs @@ -1129,8 +1129,8 @@ pub fn emit_header_injection_harness(spec: &HarnessSpec) -> HarnessSource { } else { spec.entry_name.clone() }; - let uses_rack = entry_source.contains("require 'rack'") - || entry_source.contains("require \"rack\""); + let uses_rack = + entry_source.contains("require 'rack'") || entry_source.contains("require \"rack\""); let via_fixture = if uses_rack { format!( r#"def _nyx_header_via_fixture(payload) @@ -1442,8 +1442,8 @@ pub fn emit_open_redirect_harness(spec: &HarnessSpec) -> HarnessSource { } else { spec.entry_name.clone() }; - let uses_rack = entry_source.contains("require 'rack'") - || entry_source.contains("require \"rack\""); + let uses_rack = + entry_source.contains("require 'rack'") || entry_source.contains("require \"rack\""); let via_fixture = if uses_rack { format!( r#"def _nyx_redirect_via_fixture(payload) @@ -2124,10 +2124,7 @@ mod tests { def run(value)\n r = Rack::Response.new\n r.set_header('Set-Cookie', value)\n r\nend\n", ) .unwrap(); - let h = emit_header_injection_harness(&make_header_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_header_injection_harness(&make_header_spec(entry.to_str().unwrap(), "run")); assert!( h.source.contains("def _nyx_header_via_fixture(payload)"), "tier-(a) harness must define the fixture-routing helper: {}", @@ -2149,12 +2146,14 @@ mod tests { h.source ); assert!( - h.source.contains("captured = _nyx_header_via_fixture(payload)"), + h.source + .contains("captured = _nyx_header_via_fixture(payload)"), "harness main must call the fixture-routing helper first: {}", h.source ); assert!( - h.source.contains("value = payload\n _nyx_header_probe(name, value)"), + h.source + .contains("value = payload\n _nyx_header_probe(name, value)"), "fallback path must keep the synthetic probe: {}", h.source ); @@ -2168,10 +2167,7 @@ mod tests { std::fs::create_dir_all(&dir).unwrap(); let entry = dir.join("vuln.rb"); std::fs::write(&entry, "def run(value)\n value\nend\n").unwrap(); - let h = emit_header_injection_harness(&make_header_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_header_injection_harness(&make_header_spec(entry.to_str().unwrap(), "run")); assert!( !h.source.contains("Rack::Response.prepend"), "fallback path must not patch Rack::Response: {}", @@ -2183,7 +2179,8 @@ mod tests { h.source ); assert!( - h.source.contains("value = payload\n _nyx_header_probe(name, value)"), + h.source + .contains("value = payload\n _nyx_header_probe(name, value)"), "fallback path must keep the synthetic probe: {}", h.source ); @@ -2202,10 +2199,7 @@ mod tests { def run(v)\n Rack::Response.new\nend\n", ) .unwrap(); - let h = emit_header_injection_harness(&make_header_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_header_injection_harness(&make_header_spec(entry.to_str().unwrap(), "run")); assert!( h.source.contains("require_relative './benign'"), "basename must come from the entry-file stem: {}", @@ -2228,12 +2222,10 @@ mod tests { def run_once(server)\n s = server.accept\n s.write('HTTP/1.0 200 OK\\r\\nSet-Cookie: ' + $nyx_cookie_value + '\\r\\n\\r\\nok')\n s.close\nend\n", ) .unwrap(); - let h = emit_header_injection_harness(&make_header_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_header_injection_harness(&make_header_spec(entry.to_str().unwrap(), "run")); assert!( - h.source.contains("def _nyx_wire_frame_via_fixture(payload)"), + h.source + .contains("def _nyx_wire_frame_via_fixture(payload)"), "tier-(b) harness must define the wire-frame helper: {}", h.source ); @@ -2243,7 +2235,8 @@ mod tests { h.source ); assert!( - h.source.contains("obj.__send__(:set_cookie_value, payload)"), + h.source + .contains("obj.__send__(:set_cookie_value, payload)"), "tier-(b) harness must install the cookie value via __send__: {}", h.source ); @@ -2273,7 +2266,8 @@ mod tests { h.source ); assert!( - h.source.contains("'kind' => 'HeaderWireFrame', 'raw_bytes' => raw_bytes.bytes"), + h.source + .contains("'kind' => 'HeaderWireFrame', 'raw_bytes' => raw_bytes.bytes"), "tier-(b) harness must emit a HeaderWireFrame probe carrying the raw header-block bytes: {}", h.source ); @@ -2302,10 +2296,7 @@ mod tests { def run(value)\n r = Rack::Response.new\n r.set_header('Set-Cookie', value)\n r\nend\n", ) .unwrap(); - let h = emit_header_injection_harness(&make_header_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_header_injection_harness(&make_header_spec(entry.to_str().unwrap(), "run")); assert!( !h.source.contains("def _nyx_wire_frame_via_fixture"), "rack-only harness must not define the wire-frame helper: {}", @@ -2336,10 +2327,7 @@ mod tests { def run(value)\n r = Rack::Response.new\n r.redirect(value)\n r\nend\n", ) .unwrap(); - let h = emit_open_redirect_harness(&make_redirect_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_open_redirect_harness(&make_redirect_spec(entry.to_str().unwrap(), "run")); assert!( h.source.contains("def _nyx_redirect_via_fixture(payload)"), "tier-(a) harness must define the fixture-routing helper: {}", @@ -2361,12 +2349,14 @@ mod tests { h.source ); assert!( - h.source.contains("captured = _nyx_redirect_via_fixture(payload)"), + h.source + .contains("captured = _nyx_redirect_via_fixture(payload)"), "harness main must call the fixture-routing helper first: {}", h.source ); assert!( - h.source.contains("location = payload\n _nyx_redirect_probe(location, request_host)"), + h.source + .contains("location = payload\n _nyx_redirect_probe(location, request_host)"), "fallback path must keep the synthetic probe: {}", h.source ); @@ -2380,10 +2370,7 @@ mod tests { std::fs::create_dir_all(&dir).unwrap(); let entry = dir.join("vuln.rb"); std::fs::write(&entry, "def run(value)\n value\nend\n").unwrap(); - let h = emit_open_redirect_harness(&make_redirect_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_open_redirect_harness(&make_redirect_spec(entry.to_str().unwrap(), "run")); assert!( !h.source.contains("Rack::Response.prepend"), "fallback path must not patch Rack::Response: {}", @@ -2395,7 +2382,8 @@ mod tests { h.source ); assert!( - h.source.contains("location = payload\n _nyx_redirect_probe(location, request_host)"), + h.source + .contains("location = payload\n _nyx_redirect_probe(location, request_host)"), "fallback path must keep the synthetic probe: {}", h.source ); @@ -2414,10 +2402,7 @@ mod tests { def run(value)\n r = Rack::Response.new\n r.redirect(value)\n r\nend\n", ) .unwrap(); - let h = emit_open_redirect_harness(&make_redirect_spec( - entry.to_str().unwrap(), - "run", - )); + let h = emit_open_redirect_harness(&make_redirect_spec(entry.to_str().unwrap(), "run")); assert!( h.source.contains("def _nyx_follow_location(location)"), "OPEN_REDIRECT harness must declare the _nyx_follow_location helper: {}", @@ -2441,7 +2426,9 @@ mod tests { h.source ); assert!( - h.source.contains("_nyx_redirect_probe(location, request_host)\n _nyx_follow_location(location)"), + h.source.contains( + "_nyx_redirect_probe(location, request_host)\n _nyx_follow_location(location)" + ), "tier-(a) must follow the captured Location after emitting the probe: {}", h.source ); diff --git a/src/dynamic/lang/rust.rs b/src/dynamic/lang/rust.rs index 12e97f9d..b9872a7e 100644 --- a/src/dynamic/lang/rust.rs +++ b/src/dynamic/lang/rust.rs @@ -2491,7 +2491,9 @@ mod tests { assert!(entry_source_imports_axum_header( "let h: http::HeaderMap = HeaderMap::new();" )); - assert!(!entry_source_imports_axum_header("use std::collections::HashMap;")); + assert!(!entry_source_imports_axum_header( + "use std::collections::HashMap;" + )); } #[test] @@ -2552,7 +2554,10 @@ mod tests { "tier-(a) header_injection must stage src/entry.rs", ); assert!( - staged.unwrap().1.contains("crate::nyx_harness_stubs::HeaderMap"), + staged + .unwrap() + .1 + .contains("crate::nyx_harness_stubs::HeaderMap"), "staged fixture must have axum imports rewritten", ); // Stub module staged. @@ -2620,12 +2625,16 @@ mod tests { body = harness.source, ); assert!( - harness.source.contains("entry::set_cookie_value(payload.as_bytes())"), + harness + .source + .contains("entry::set_cookie_value(payload.as_bytes())"), "wire-frame harness must install cookie value on the fixture: {body}", body = harness.source, ); assert!( - harness.source.contains("let listener = entry::create_server();"), + harness + .source + .contains("let listener = entry::create_server();"), "wire-frame harness must boot the fixture's TcpListener: {body}", body = harness.source, ); @@ -2663,10 +2672,7 @@ mod tests { assert_eq!(harness.entry_subpath.as_deref(), Some("src/entry.rs")); // Cargo.toml must still be staged so the workdir builds. assert!( - harness - .extra_files - .iter() - .any(|(p, _)| p == "Cargo.toml"), + harness.extra_files.iter().any(|(p, _)| p == "Cargo.toml"), "wire-frame harness must stage Cargo.toml: {files:?}", files = harness .extra_files @@ -2752,7 +2758,10 @@ mod tests { .extra_files .iter() .find(|(p, _)| p == "src/entry.rs"); - assert!(staged.is_some(), "tier-(a) open_redirect must stage src/entry.rs"); + assert!( + staged.is_some(), + "tier-(a) open_redirect must stage src/entry.rs" + ); assert!( staged .unwrap() @@ -2764,7 +2773,10 @@ mod tests { .extra_files .iter() .find(|(p, _)| p == "src/nyx_harness_stubs.rs"); - assert!(stub.is_some(), "tier-(a) open_redirect must stage nyx_harness_stubs.rs"); + assert!( + stub.is_some(), + "tier-(a) open_redirect must stage nyx_harness_stubs.rs" + ); assert_eq!( harness.entry_subpath.as_deref(), Some("ignored/raw_fixture.rs"), @@ -2805,7 +2817,9 @@ mod tests { spec.entry_file = "/nonexistent/missing.rs".into(); let harness = emit_open_redirect_harness(&spec); assert!( - harness.source.contains("fn nyx_follow_location(location: &str)"), + harness + .source + .contains("fn nyx_follow_location(location: &str)"), "OPEN_REDIRECT harness must declare the nyx_follow_location helper", ); for prefix in [ @@ -2830,9 +2844,9 @@ mod tests { ); // Tier-(b) callsite must call the follower on the synthetic payload. assert!( - harness - .source - .contains("nyx_redirect_probe(&location, request_host);\n nyx_follow_location(&location);"), + harness.source.contains( + "nyx_redirect_probe(&location, request_host);\n nyx_follow_location(&location);" + ), "tier-(b) callsite must invoke nyx_follow_location after the synthetic probe", ); } @@ -2846,7 +2860,9 @@ mod tests { let harness = emit_open_redirect_harness(&spec); // Tier-(a) callsite: captured loc → probe + follow. assert!( - harness.source.contains("nyx_redirect_probe(&location, request_host);\n nyx_follow_location(&location);"), + harness.source.contains( + "nyx_redirect_probe(&location, request_host);\n nyx_follow_location(&location);" + ), "tier-(a) callsite must invoke nyx_follow_location on the captured Location", ); } @@ -3003,7 +3019,8 @@ mod tests { h.source ); assert!( - h.source.contains("impl NyxKeyToInt for [u8; N]"), + h.source + .contains("impl NyxKeyToInt for [u8; N]"), "Rust CRYPTO harness must provide a generic [u8; N] impl so both [u8; 32] (benign) and other-sized array returns reduce uniformly: {}", h.source ); diff --git a/src/dynamic/middleware_demotion.rs b/src/dynamic/middleware_demotion.rs index a468c041..3ab365a3 100644 --- a/src/dynamic/middleware_demotion.rs +++ b/src/dynamic/middleware_demotion.rs @@ -29,8 +29,8 @@ //! guard names on [`DifferentialOutcome::known_guards`] and can //! deprioritise the finding without losing the underlying signal. -use crate::dynamic::framework::auth_markers::{AuthMarkerKind, classify}; use crate::dynamic::framework::FrameworkBinding; +use crate::dynamic::framework::auth_markers::{AuthMarkerKind, classify}; use crate::evidence::{DifferentialOutcome, DifferentialVerdict}; use crate::symbol::Lang; @@ -116,9 +116,7 @@ pub fn is_triggering_verdict(verdict: DifferentialVerdict) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::dynamic::framework::{ - FrameworkBinding, HttpMethod, MiddlewareShape, RouteShape, - }; + use crate::dynamic::framework::{FrameworkBinding, HttpMethod, MiddlewareShape, RouteShape}; use crate::evidence::EntryKind; fn make_outcome(verdict: DifferentialVerdict) -> DifferentialOutcome { diff --git a/src/dynamic/oracle.rs b/src/dynamic/oracle.rs index 085e7445..e348c78b 100644 --- a/src/dynamic/oracle.rs +++ b/src/dynamic/oracle.rs @@ -1665,9 +1665,7 @@ mod tests { smuggled: "X-Injected", }], }; - let probes = vec![header_wire_probe( - b"Set-Cookie: a=1\r\nX-Injected: 1\r\n", - )]; + let probes = vec![header_wire_probe(b"Set-Cookie: a=1\r\nX-Injected: 1\r\n")]; assert!(oracle_fired(&oracle, &outcome(), &probes)); } @@ -1696,9 +1694,7 @@ mod tests { smuggled: "x-injected", }], }; - let probes = vec![header_wire_probe( - b"SET-COOKIE: a=1\r\nX-INJECTED: 1\r\n", - )]; + let probes = vec![header_wire_probe(b"SET-COOKIE: a=1\r\nX-INJECTED: 1\r\n")]; assert!(oracle_fired(&oracle, &outcome(), &probes)); } @@ -1714,10 +1710,7 @@ mod tests { smuggled: "X-Injected", }], }; - let probes = vec![header_emit_probe( - "Set-Cookie", - "a=1\r\nX-Injected: 1", - )]; + let probes = vec![header_emit_probe("Set-Cookie", "a=1\r\nX-Injected: 1")]; assert!(!oracle_fired(&oracle, &outcome(), &probes)); } @@ -1731,9 +1724,7 @@ mod tests { header_name: "Set-Cookie", }], }; - let probes = vec![header_wire_probe( - b"Set-Cookie: a=1\r\nX-Injected: 1\r\n", - )]; + let probes = vec![header_wire_probe(b"Set-Cookie: a=1\r\nX-Injected: 1\r\n")]; assert!(!oracle_fired(&oracle, &outcome(), &probes)); } diff --git a/src/dynamic/runner.rs b/src/dynamic/runner.rs index 873654a8..c341ec48 100644 --- a/src/dynamic/runner.rs +++ b/src/dynamic/runner.rs @@ -137,10 +137,7 @@ fn is_runtime_import_error(outcome: &sandbox::SandboxOutcome) -> bool { return false; } let needle = b"NYX_IMPORT_ERROR:"; - outcome - .stderr - .windows(needle.len()) - .any(|w| w == needle) + outcome.stderr.windows(needle.len()).any(|w| w == needle) } /// Build harness (with retry), run every payload, stop at first confirmed trigger. @@ -711,7 +708,10 @@ mod tests { #[test] fn import_error_detects_exit_77_with_marker() { - let outcome = outcome_with(Some(77), b"NYX_IMPORT_ERROR: Cannot find module 'express'\n"); + let outcome = outcome_with( + Some(77), + b"NYX_IMPORT_ERROR: Cannot find module 'express'\n", + ); assert!(is_runtime_import_error(&outcome)); } diff --git a/src/dynamic/spec.rs b/src/dynamic/spec.rs index 404ba925..93d62562 100644 --- a/src/dynamic/spec.rs +++ b/src/dynamic/spec.rs @@ -1239,9 +1239,8 @@ fn attach_framework_binding(spec: &mut HarnessSpec, summaries: Option<&GlobalSum let resolved = summaries .and_then(|gs| find_summary_by_path(gs, spec.lang, &spec.entry_name, &spec.entry_file)); let summary_ref = resolved.unwrap_or(&synthetic); - let ssa_ref = summaries.and_then(|gs| { - find_ssa_summary_by_path(gs, spec.lang, &spec.entry_name, &spec.entry_file) - }); + let ssa_ref = summaries + .and_then(|gs| find_ssa_summary_by_path(gs, spec.lang, &spec.entry_name, &spec.entry_file)); if let Some(binding) = crate::dynamic::framework::detect_binding_with_context( summary_ref, ssa_ref, diff --git a/src/dynamic/stubs/ldap_server.rs b/src/dynamic/stubs/ldap_server.rs index 77bc5ed1..6d2ccb86 100644 --- a/src/dynamic/stubs/ldap_server.rs +++ b/src/dynamic/stubs/ldap_server.rs @@ -290,8 +290,7 @@ fn handle_ber_connection( let count = matches.len(); for uid in &matches { let dn = format!("uid={uid},ou=people,dc=nyx,dc=test"); - let entry = - ldap_ber::encode_search_result_entry(hdr.message_id, dn.as_bytes()); + let entry = ldap_ber::encode_search_result_entry(hdr.message_id, dn.as_bytes()); if stream.write_all(&entry).is_err() { return; } @@ -696,8 +695,12 @@ mod tests { let mut eq_body = Vec::new(); ldap_ber::write_octet_string(&mut eq_body, b"uid"); ldap_ber::write_octet_string(&mut eq_body, b"alice"); - s.write_all(&build_ber_search(2, ldap_ber::tags::FILTER_EQUALITY, &eq_body)) - .unwrap(); + s.write_all(&build_ber_search( + 2, + ldap_ber::tags::FILTER_EQUALITY, + &eq_body, + )) + .unwrap(); s.shutdown(std::net::Shutdown::Write).unwrap(); let reply = read_ber_reply(&mut s); // Skip past the BindResponse. diff --git a/tests/dynamic_fixtures/json_parse_depth/python/vuln.py b/tests/dynamic_fixtures/json_parse_depth/python/vuln.py new file mode 100644 index 00000000..33582375 --- /dev/null +++ b/tests/dynamic_fixtures/json_parse_depth/python/vuln.py @@ -0,0 +1,23 @@ +# Python 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.loads` +# 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. +import json + + +def run(value): + if isinstance(value, (bytes, bytearray)): + value = value.decode("utf-8", "replace") + elif not isinstance(value, str): + value = str(value) + if "DEEP" in value: + nested = "[" * 256 + "]" * 256 + return json.loads(nested) + return json.loads("[]") diff --git a/tests/header_injection_corpus.rs b/tests/header_injection_corpus.rs index 13229bd5..726ec2a1 100644 --- a/tests/header_injection_corpus.rs +++ b/tests/header_injection_corpus.rs @@ -785,17 +785,16 @@ mod e2e_phase_08 { let outcome = match run_spec(&spec, &opts) { Ok(outcome) => outcome, Err(RunError::BuildFailed { stderr, attempts }) => { - eprintln!( - "SKIP js_raw: harness build failed after {attempts} attempts: {stderr}", - ); + eprintln!("SKIP js_raw: harness build failed after {attempts} attempts: {stderr}",); return; } Err(e) => panic!("run_spec(js_raw) errored: {e:?}"), }; assert_confirmed(Lang::JavaScript, &outcome); - let any_wire_frame_marker = outcome.attempts.iter().any(|a| { - String::from_utf8_lossy(&a.outcome.stdout).contains("wire_frame_len") - }); + let any_wire_frame_marker = outcome + .attempts + .iter() + .any(|a| String::from_utf8_lossy(&a.outcome.stdout).contains("wire_frame_len")); assert!( any_wire_frame_marker, "js_raw fixture must exercise the tier-(b) wire-frame harness branch; \ @@ -882,9 +881,10 @@ mod e2e_phase_08 { Err(e) => panic!("run_spec(rust_raw) errored: {e:?}"), }; assert_confirmed(Lang::Rust, &outcome); - let any_wire_frame_marker = outcome.attempts.iter().any(|a| { - String::from_utf8_lossy(&a.outcome.stdout).contains("wire_frame_len") - }); + let any_wire_frame_marker = outcome + .attempts + .iter() + .any(|a| String::from_utf8_lossy(&a.outcome.stdout).contains("wire_frame_len")); assert!( any_wire_frame_marker, "rust_raw fixture must exercise the tier-(b) wire-frame harness branch; \ @@ -920,9 +920,10 @@ mod e2e_phase_08 { Err(e) => panic!("run_spec(python_raw) errored: {e:?}"), }; assert_confirmed(Lang::Python, &outcome); - let any_wire_frame_marker = outcome.attempts.iter().any(|a| { - String::from_utf8_lossy(&a.outcome.stdout).contains("wire_frame_len") - }); + let any_wire_frame_marker = outcome + .attempts + .iter() + .any(|a| String::from_utf8_lossy(&a.outcome.stdout).contains("wire_frame_len")); assert!( any_wire_frame_marker, "python_raw fixture must exercise the tier-(b) wire-frame harness branch; \ @@ -1003,9 +1004,10 @@ mod e2e_phase_08 { Err(e) => panic!("run_spec(ruby_raw) errored: {e:?}"), }; assert_confirmed(Lang::Ruby, &outcome); - let any_wire_frame_marker = outcome.attempts.iter().any(|a| { - String::from_utf8_lossy(&a.outcome.stdout).contains("wire_frame_len") - }); + let any_wire_frame_marker = outcome + .attempts + .iter() + .any(|a| String::from_utf8_lossy(&a.outcome.stdout).contains("wire_frame_len")); assert!( any_wire_frame_marker, "ruby_raw fixture must exercise the tier-(b) wire-frame harness branch; \ @@ -1079,17 +1081,16 @@ mod e2e_phase_08 { let outcome = match run_spec(&spec, &opts) { Ok(outcome) => outcome, Err(RunError::BuildFailed { stderr, attempts }) => { - eprintln!( - "SKIP php_raw: harness build failed after {attempts} attempts: {stderr}", - ); + eprintln!("SKIP php_raw: harness build failed after {attempts} attempts: {stderr}",); return; } Err(e) => panic!("run_spec(php_raw) errored: {e:?}"), }; assert_confirmed(Lang::Php, &outcome); - let any_wire_frame_marker = outcome.attempts.iter().any(|a| { - String::from_utf8_lossy(&a.outcome.stdout).contains("wire_frame_len") - }); + let any_wire_frame_marker = outcome + .attempts + .iter() + .any(|a| String::from_utf8_lossy(&a.outcome.stdout).contains("wire_frame_len")); assert!( any_wire_frame_marker, "php_raw fixture must exercise the tier-(b) wire-frame harness branch; \ @@ -1182,9 +1183,10 @@ mod e2e_phase_08 { Err(e) => panic!("run_spec(java_raw) errored: {e:?}"), }; assert_confirmed(Lang::Java, &outcome); - let any_wire_frame_marker = outcome.attempts.iter().any(|a| { - String::from_utf8_lossy(&a.outcome.stdout).contains("wire_frame_len") - }); + let any_wire_frame_marker = outcome + .attempts + .iter() + .any(|a| String::from_utf8_lossy(&a.outcome.stdout).contains("wire_frame_len")); assert!( any_wire_frame_marker, "java_raw fixture must exercise the tier-(b) wire-frame harness branch; \ diff --git a/tests/json_parse_corpus.rs b/tests/json_parse_corpus.rs index c73a3410..29963914 100644 --- a/tests/json_parse_corpus.rs +++ b/tests/json_parse_corpus.rs @@ -10,6 +10,8 @@ #![cfg(feature = "dynamic")] +mod common; + use nyx_scanner::dynamic::corpus::{payloads_for_lang, resolve_benign_control_lang}; use nyx_scanner::dynamic::oracle::{Oracle, ProbePredicate, oracle_fired}; use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe}; @@ -97,6 +99,117 @@ fn canary_predicate_fires_only_on_canary_property() { assert!(!oracle_fired(&oracle, &outcome(), &[])); } +// Runs the depth-bomb fixture through the dynamic runner. The same fixture +// handles the vulnerable and benign payloads; the payload tag picks the branch. +mod e2e_json_parse_depth { + use crate::common::fixture_harness::FIXTURE_LOCK; + use nyx_scanner::dynamic::runner::{RunError, RunOutcome, run_spec}; + use nyx_scanner::dynamic::sandbox::{SandboxBackend, SandboxOptions}; + use nyx_scanner::dynamic::spec::{ + EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, default_toolchain_id, + }; + use nyx_scanner::evidence::DifferentialVerdict; + use nyx_scanner::labels::Cap; + use nyx_scanner::symbol::Lang; + use std::path::PathBuf; + use std::process::Command; + use tempfile::TempDir; + + fn command_available(bin: &str) -> bool { + Command::new(bin) + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + } + + fn build_spec(lang: Lang, fixture: &str, entry_name: &str) -> (HarnessSpec, TempDir) { + let fixture_src = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/dynamic_fixtures/json_parse_depth") + .join(match lang { + Lang::Python => "python", + _ => unreachable!("JSON_PARSE depth e2e covers Python only"), + }) + .join(fixture); + let tmp = TempDir::new().expect("create tempdir"); + let dst = tmp.path().join(fixture); + std::fs::copy(&fixture_src, &dst).expect("copy fixture into tempdir"); + + let entry_file = dst.to_string_lossy().into_owned(); + let mut digest = blake3::Hasher::new(); + digest.update(b"e2e-json-parse|"); + digest.update(fixture.as_bytes()); + let spec_hash = format!("{:016x}", { + let bytes = digest.finalize(); + u64::from_le_bytes(bytes.as_bytes()[..8].try_into().unwrap()) + }); + + let spec = HarnessSpec { + finding_id: spec_hash.clone(), + entry_file: entry_file.clone(), + entry_name: entry_name.to_owned(), + entry_kind: EntryKind::Function, + lang, + toolchain_id: default_toolchain_id(lang).into(), + payload_slot: PayloadSlot::Param(0), + expected_cap: Cap::JSON_PARSE, + constraint_hints: vec![], + sink_file: entry_file, + sink_line: 1, + spec_hash: spec_hash.clone(), + derivation: SpecDerivationStrategy::FromFlowSteps, + stubs_required: vec![], + framework: None, + java_toolchain: nyx_scanner::dynamic::spec::JavaToolchain::default(), + }; + + (spec, tmp) + } + + fn run(lang: Lang, fixture: &str, entry_name: &str) -> Option { + if !command_available("python3") { + eprintln!("SKIP {lang:?} {fixture}: missing toolchain python3"); + return None; + } + let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let (spec, _tmp) = build_spec(lang, fixture, entry_name); + let opts = SandboxOptions { + backend: SandboxBackend::Process, + ..SandboxOptions::default() + }; + match run_spec(&spec, &opts) { + Ok(outcome) => Some(outcome), + Err(RunError::BuildFailed { stderr, attempts }) => { + eprintln!( + "SKIP {lang:?} {fixture}: harness build failed after {attempts} attempts: {stderr}", + ); + None + } + Err(e) => panic!("run_spec({lang:?} {fixture}) errored: {e:?}"), + } + } + + fn assert_confirmed(lang: Lang, outcome: &RunOutcome) { + assert!( + outcome.triggered_by.is_some(), + "{lang:?} JSON_PARSE depth bomb must confirm via run_spec; got {outcome:?}", + ); + let diff = outcome + .differential + .as_ref() + .expect("confirmed run must carry a DifferentialOutcome"); + assert_eq!(diff.verdict, DifferentialVerdict::Confirmed); + } + + #[test] + fn python_vuln_confirms_via_run_spec() { + let Some(outcome) = run(Lang::Python, "vuln.py", "run") else { + return; + }; + assert_confirmed(Lang::Python, &outcome); + } +} + #[test] fn json_parse_unsupported_for_other_langs() { for lang in [