docs(configuration): improve clarity and formatting in configuration documentation

This commit is contained in:
elipeter 2026-05-22 09:42:18 -05:00
parent 9062cd652a
commit 32211079a0
32 changed files with 717 additions and 380 deletions

View file

@ -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,
},
];

View file

@ -277,9 +277,7 @@ pub fn collect_use_middleware(root: Node<'_>, bytes: &[u8]) -> Vec<MiddlewareSha
walk_use_calls(root, bytes, &mut raw);
let mut out: Vec<MiddlewareShape> = 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());

View file

@ -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\

View file

@ -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);

View file

@ -72,9 +72,7 @@ fn extract_version(file_bytes: &[u8]) -> Option<String> {
.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);
}

View file

@ -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 {

View file

@ -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
);

View file

@ -698,9 +698,7 @@ pub fn collect_php_middleware(root: Node<'_>, bytes: &[u8]) -> Vec<MiddlewareSha
walk_php_middleware(root, bytes, &mut raw);
let mut out: Vec<MiddlewareShape> = 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"<?php\nRoute::get('/x', 'C@x')->middleware(['auth', 'verified']);\n";
let src: &[u8] = b"<?php\nRoute::get('/x', 'C@x')->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:?}");

View file

@ -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
);

View file

@ -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();

View file

@ -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<MiddlewareSh
walk_attach_calls(root, bytes, &mut raw);
let mut out: Vec<MiddlewareShape> = 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.

View file

@ -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
);

View file

@ -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]

View file

@ -318,9 +318,7 @@ pub fn collect_rust_middleware(root: Node<'_>, bytes: &[u8]) -> Vec<MiddlewareSh
walk_attach_calls(root, bytes, &mut raw);
let mut out: Vec<MiddlewareShape> = 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");

View file

@ -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]

View file

@ -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.

View file

@ -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!(

View file

@ -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",
);
}

View file

@ -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",
);
}

View file

@ -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
);

View file

@ -2913,10 +2913,7 @@ mod tests {
"<?php\nfunction run($value) {\n header(\"Set-Cookie: \" . $value);\n}\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 _nyx_header_via_fixture("),
"tier-(a) harness must define the fixture-routing helper: {}",
@ -2943,7 +2940,8 @@ mod tests {
h.source
);
assert!(
h.source.contains("$value = $payload;\n _nyx_header_probe("),
h.source
.contains("$value = $payload;\n _nyx_header_probe("),
"fallback path must keep the synthetic probe: {}",
h.source
);
@ -2957,10 +2955,7 @@ mod tests {
std::fs::create_dir_all(&dir).unwrap();
let entry = dir.join("vuln.php");
std::fs::write(&entry, "<?php\nfunction run($v) { return $v; }\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 _nyx_header_via_fixture("),
"fallback path must not define the fixture-routing helper: {}",
@ -2972,7 +2967,8 @@ mod tests {
h.source
);
assert!(
h.source.contains("$value = $payload;\n _nyx_header_probe("),
h.source
.contains("$value = $payload;\n _nyx_header_probe("),
"fallback path must keep the synthetic probe: {}",
h.source
);
@ -2990,10 +2986,7 @@ mod tests {
"<?php\nfunction run($value) {\n header(\"Set-Cookie: \" . urlencode($value));\n}\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("\"benign.php\""),
"tier-(a) harness must use the entry-file basename: {}",
@ -3017,10 +3010,7 @@ mod tests {
function run_once($server) { $c = stream_socket_accept($server, 5.0); if ($c === false) return; fwrite($c, \"HTTP/1.0 200 OK\\r\\nSet-Cookie: \" . $GLOBALS['nyx_cookie_value'] . \"\\r\\n\\r\\nok\"); fclose($c); }\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 _nyx_wire_frame_via_fixture("),
"tier-(b) harness must define the wire-frame helper: {}",
@ -3062,7 +3052,8 @@ mod tests {
h.source
);
assert!(
h.source.contains("'kind' => '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 {
"<?php\nfunction run($value) {\n header(\"Set-Cookie: \" . $value);\n}\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 _nyx_wire_frame_via_fixture("),
"header()-only harness must not define the wire-frame helper: {}",
@ -3123,10 +3111,7 @@ mod tests {
"<?php\nuse Symfony\\Component\\HttpFoundation\\RedirectResponse;\nfunction run(string $value): RedirectResponse {\n return new RedirectResponse($value);\n}\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 _nyx_redirect_via_fixture("),
"tier-(a) harness must define the fixture-routing helper: {}",
@ -3158,7 +3143,8 @@ mod tests {
h.source
);
assert!(
h.source.contains("$location = $payload;\n _nyx_redirect_probe("),
h.source
.contains("$location = $payload;\n _nyx_redirect_probe("),
"fallback path must keep the synthetic probe: {}",
h.source
);
@ -3172,10 +3158,7 @@ mod tests {
std::fs::create_dir_all(&dir).unwrap();
let entry = dir.join("vuln.php");
std::fs::write(&entry, "<?php\nfunction run($v) { return $v; }\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 _nyx_redirect_via_fixture("),
"fallback path must not define the fixture-routing helper: {}",
@ -3187,7 +3170,8 @@ mod tests {
h.source
);
assert!(
h.source.contains("$location = $payload;\n _nyx_redirect_probe("),
h.source
.contains("$location = $payload;\n _nyx_redirect_probe("),
"fallback path must keep the synthetic probe: {}",
h.source
);
@ -3205,17 +3189,16 @@ mod tests {
"<?php\nuse Symfony\\Component\\HttpFoundation\\RedirectResponse;\nfunction run($v) { return new RedirectResponse($v); }\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 _nyx_follow_location(string $location): void"),
h.source
.contains("function _nyx_follow_location(string $location): void"),
"OPEN_REDIRECT harness must declare the _nyx_follow_location helper: {}",
h.source
);
assert!(
h.source.contains("file_get_contents($location, false, $ctx)"),
h.source
.contains("file_get_contents($location, false, $ctx)"),
"follow-location helper must call file_get_contents with a stream context: {}",
h.source
);
@ -3225,9 +3208,12 @@ mod tests {
h.source
);
assert!(
h.source.contains("str_starts_with($lower, 'http://127.0.0.1')")
&& h.source.contains("str_starts_with($lower, 'http://localhost')")
&& h.source.contains("str_starts_with($lower, 'http://host-gateway')"),
h.source
.contains("str_starts_with($lower, 'http://127.0.0.1')")
&& h.source
.contains("str_starts_with($lower, 'http://localhost')")
&& h.source
.contains("str_starts_with($lower, 'http://host-gateway')"),
"follow-location helper must gate on loopback host prefixes: {}",
h.source
);
@ -3315,7 +3301,8 @@ mod tests {
"run",
));
assert!(
h.source.contains("['kind' => '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
);

View file

@ -700,6 +700,11 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
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\")"));
}
}

View file

@ -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
);

View file

@ -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<const N: usize> NyxKeyToInt for [u8; N]"),
h.source
.contains("impl<const N: usize> 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
);

View file

@ -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 {

View file

@ -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));
}

View file

@ -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));
}

View file

@ -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,

View file

@ -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.

View file

@ -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("[]")

View file

@ -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; \

View file

@ -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<RunOutcome> {
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 [