mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
docs(configuration): improve clarity and formatting in configuration documentation
This commit is contained in:
parent
9062cd652a
commit
32211079a0
32 changed files with 717 additions and 380 deletions
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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\
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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:?}");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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\")"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
23
tests/dynamic_fixtures/json_parse_depth/python/vuln.py
Normal file
23
tests/dynamic_fixtures/json_parse_depth/python/vuln.py
Normal 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("[]")
|
||||
|
|
@ -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; \
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue