From b0d5527174fc51a4921514febc26ec9b8d87bd3d Mon Sep 17 00:00:00 2001 From: pitboss Date: Thu, 21 May 2026 21:50:01 -0500 Subject: [PATCH] [pitboss/grind] deferred session-0023 (20260521T201327Z-3848) --- src/dynamic/lang/go.rs | 438 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 406 insertions(+), 32 deletions(-) diff --git a/src/dynamic/lang/go.rs b/src/dynamic/lang/go.rs index 322e3be6..48b0b2fa 100644 --- a/src/dynamic/lang/go.rs +++ b/src/dynamic/lang/go.rs @@ -758,13 +758,61 @@ func main() {{ /// Phase 08 — Track J.6 header-injection harness for Go /// (`http.ResponseWriter.Header().Set`). /// -/// Reads `NYX_PAYLOAD`, calls a synthetic instrumented `Header.Set` -/// shim that records the *unmodified* value bytes (including any -/// embedded `\r\n`) via a `ProbeKind::HeaderEmit` probe. Mirrors -/// the synthetic-harness pattern used by Phase 05. -pub fn emit_header_injection_harness(_spec: &HarnessSpec) -> HarnessSource { +/// Tier (a): when the fixture imports `net/http` and exposes a +/// `func (w http.ResponseWriter, value string)`, the harness +/// rewrites the fixture's `package ` declaration to +/// `package vulnentry`, stages the rewritten copy under +/// `internal/vulnentry/`, drives the fixture against +/// `httptest.NewRecorder()`, and emits one `ProbeKind::HeaderEmit` +/// probe per `(name, value)` pair captured on the response writer. +/// +/// Tier (b) (fallback): when the fixture does not import `net/http`, +/// inlines a synthetic `nyxHeaderProbe("Set-Cookie", payload)` so the +/// differential oracle still flips on raw payload bytes. Mirrors the +/// Java / Python / Node / Ruby / PHP tier-(a) + synthetic-fallback +/// dispatch pattern landed in earlier sessions. +pub fn emit_header_injection_harness(spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); let go_mod = generate_go_mod(); + let entry_fn = capitalize_first(&spec.entry_name); + let entry_source = read_entry_source(&spec.entry_file); + let tier_a_active = entry_source_imports_net_http(&entry_source); + + let mut extra_imports = ""; + let mut via_fixture_decl = String::new(); + let via_fixture_invoke; + let mut extra_files = vec![("go.mod".to_owned(), go_mod)]; + + 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"; + via_fixture_decl = format!( + r##"func nyxHeaderViaFixture(payload string) bool {{ + defer func() {{ _ = recover() }}() + rec := httptest.NewRecorder() + vulnentry.{entry_fn}(rec, payload) + fired := false + for name, values := range rec.Header() {{ + for _, value := range values {{ + nyxHeaderProbe(name, value) + fired = true + }} + }} + _ = http.StatusOK + return fired +}} + +"## + ); + via_fixture_invoke = "\tif !nyxHeaderViaFixture(payload) {\n\t\tnyxHeaderProbe(\"Set-Cookie\", payload)\n\t}\n".to_owned(); + } else { + via_fixture_invoke = "\tnyxHeaderProbe(\"Set-Cookie\", payload)\n".to_owned(); + } + let source = format!( r##"// Nyx dynamic harness — HEADER_INJECTION http.ResponseWriter.Header().Set (Phase 08 / Track J.6). package main @@ -777,7 +825,7 @@ import ( "strings" "syscall" "time" -) +{extra_imports}) {shim} @@ -795,15 +843,12 @@ func nyxHeaderProbe(name, value string) {{ }}) }} -func main() {{ +{via_fixture_decl}func main() {{ __nyx_install_crash_guard("http.ResponseWriter.Header.Set") defer __nyx_recover_crash("http.ResponseWriter.Header.Set")() payload := os.Getenv("NYX_PAYLOAD") - name := "Set-Cookie" - value := payload - nyxHeaderProbe(name, value) - fmt.Println("__NYX_SINK_HIT__") - body, _ := json.Marshal(map[string]interface{{}}{{"name": name, "value": value}}) +{via_fixture_invoke} fmt.Println("__NYX_SINK_HIT__") + body, _ := json.Marshal(map[string]interface{{}}{{"payload_len": len(payload)}}) fmt.Println(string(body)) }} "## @@ -812,24 +857,154 @@ func main() {{ source, filename: "main.go".to_owned(), command: vec!["./nyx_harness".to_owned()], - extra_files: vec![("go.mod".to_owned(), go_mod)], - // Park the fixture under `entry/` so `go build .` only picks up - // the synthetic `main.go` — fixtures declare `package vuln` / - // `package benign`, which would otherwise collide with the - // harness's `package main` and break the build. + extra_files, + // Park the raw fixture under `entry/` so `go build .` ignores + // it (the directory is never imported by main). When tier (a) + // fires, the rewritten copy lives under `internal/vulnentry/` + // with `package vulnentry` so main.go can import it directly. entry_subpath: Some("entry/entry.go".to_owned()), } } +/// Tier-(a) gate for HEADER_INJECTION + OPEN_REDIRECT: the fixture +/// must import `net/http` (header injection) or otherwise expose the +/// stdlib `http.ResponseWriter` / `http.Request` surface. Returns +/// `true` for any `import "net/http"` style declaration. +fn entry_source_imports_net_http(src: &str) -> bool { + src.contains("\"net/http\"") +} + +/// Rewrite the first `^package $` line in `src` to +/// `package `. Tier-(a) harnesses use this to normalise +/// per-fixture package names (`package vuln` / `package benign`) to a +/// fixed name the synthetic main.go can import. Returns the input +/// unchanged when no `package` line is found (best-effort: the build +/// will fail loudly downstream). +fn rewrite_package(src: &str, target: &str) -> String { + let mut out = String::with_capacity(src.len() + 16); + let mut rewrote = false; + for line in src.split_inclusive('\n') { + let trimmed = line.trim_end_matches(['\r', '\n']); + if !rewrote + && let Some(rest) = trimmed.strip_prefix("package ") + && !rest.trim().is_empty() + { + out.push_str("package "); + out.push_str(target); + // Preserve original line ending. + if line.ends_with("\r\n") { + out.push_str("\r\n"); + } else if line.ends_with('\n') { + out.push('\n'); + } + rewrote = true; + continue; + } + out.push_str(line); + } + out +} + /// Phase 09 — Track J.7 open-redirect harness for Go (`gin.Context.Redirect` /// / `http.Redirect`). /// -/// Reads `NYX_PAYLOAD`, calls a synthetic instrumented redirect shim -/// that records the bound `Location:` value plus the request's -/// origin host via a `ProbeKind::Redirect` probe. -pub fn emit_open_redirect_harness(_spec: &HarnessSpec) -> HarnessSource { +/// Tier (a) — gin shape: when the fixture imports +/// `github.com/gin-gonic/gin`, the harness rewrites the fixture's +/// `package ` to `package vulnentry`, rewrites the `gin` import to a +/// local stub path, stages the rewritten fixture + gin stub copy +/// under `internal/vulnentry/`, constructs +/// `gin.NewContext(httptest.NewRecorder(), req)`, calls +/// `vulnentry.(ctx, payload)`, and emits a `ProbeKind::Redirect` +/// probe carrying the `Location:` value the stub captured. +/// +/// Tier (a) — stdlib shape: when the fixture imports `net/http` +/// directly (no gin), the same tier-(a) path runs minus the gin stub +/// and the harness calls +/// `vulnentry.(httptest.NewRecorder(), , payload)`. +/// +/// Tier (b) (fallback): when neither gate fires, emits a synthetic +/// `nyxRedirectProbe(payload, "example.com")` so the differential +/// oracle still flips on the raw payload. +pub fn emit_open_redirect_harness(spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); let go_mod = generate_go_mod(); + let entry_fn = capitalize_first(&spec.entry_name); + let entry_source = read_entry_source(&spec.entry_file); + let imports_gin = entry_source.contains("gin-gonic/gin"); + let imports_net_http = entry_source_imports_net_http(&entry_source); + + let mut extra_imports = String::new(); + let mut via_fixture_decl = String::new(); + let mut via_fixture_invoke = String::new(); + let mut extra_files = vec![("go.mod".to_owned(), go_mod)]; + + if imports_gin { + // Rewrite package + gin import to local stub. + let rewritten = rewrite_package(&entry_source, "vulnentry"); + let rewritten = rewritten.replace( + "\"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_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) {{ + defer func() {{ _ = recover() }}() + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", strings.NewReader("")) + ctx := gin.NewContext(rec, req) + vulnentry.{entry_fn}(ctx, payload) + loc := rec.Header().Get("Location") + if loc == "" {{ + return "", false + }} + _ = http.StatusOK + return loc, true +}} + +"## + )); + via_fixture_invoke.push_str( + "\tif loc, ok := nyxRedirectViaFixture(payload); ok {\n\t\tnyxRedirectProbe(loc, requestHost)\n\t} else {\n\t\tnyxRedirectProbe(payload, requestHost)\n\t}\n", + ); + } 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"); + via_fixture_decl.push_str(&format!( + r##"func nyxRedirectViaFixture(payload string) (string, bool) {{ + defer func() {{ _ = recover() }}() + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", strings.NewReader("")) + vulnentry.{entry_fn}(rec, req, payload) + loc := rec.Header().Get("Location") + if loc == "" {{ + return "", false + }} + _ = http.StatusOK + return loc, true +}} + +"## + )); + via_fixture_invoke.push_str( + "\tif loc, ok := nyxRedirectViaFixture(payload); ok {\n\t\tnyxRedirectProbe(loc, requestHost)\n\t} else {\n\t\tnyxRedirectProbe(payload, requestHost)\n\t}\n", + ); + } else { + via_fixture_invoke.push_str("\tnyxRedirectProbe(payload, requestHost)\n"); + } + let source = format!( r##"// Nyx dynamic harness — OPEN_REDIRECT c.Redirect (Phase 09 / Track J.7). package main @@ -842,7 +1017,7 @@ import ( "strings" "syscall" "time" -) +{extra_imports}) {shim} @@ -859,15 +1034,13 @@ func nyxRedirectProbe(location, requestHost string) {{ }}) }} -func main() {{ +{via_fixture_decl}func main() {{ __nyx_install_crash_guard("gin.Context.Redirect") defer __nyx_recover_crash("gin.Context.Redirect")() payload := os.Getenv("NYX_PAYLOAD") requestHost := "example.com" - location := payload - nyxRedirectProbe(location, requestHost) - fmt.Println("__NYX_SINK_HIT__") - body, _ := json.Marshal(map[string]interface{{}}{{"location": location, "request_host": requestHost}}) +{via_fixture_invoke} fmt.Println("__NYX_SINK_HIT__") + body, _ := json.Marshal(map[string]interface{{}}{{"request_host": requestHost}}) fmt.Println(string(body)) }} "## @@ -876,11 +1049,10 @@ func main() {{ source, filename: "main.go".to_owned(), command: vec!["./nyx_harness".to_owned()], - extra_files: vec![("go.mod".to_owned(), go_mod)], - // Park the fixture under `entry/` so `go build .` only picks up - // the synthetic `main.go` — fixtures declare `package vuln` / - // `package benign`, which would otherwise collide with the - // harness's `package main` and break the build. + extra_files, + // Park the raw fixture under `entry/` so `go build .` ignores + // it (the directory is never imported by main). Tier (a) + // ships the rewritten copy under `internal/vulnentry/`. entry_subpath: Some("entry/entry.go".to_owned()), } } @@ -1489,6 +1661,13 @@ func (c *Context) String(code int, format string, values ...interface{}) { fmt.Fprintf(c.Writer, format, values...) } } + +func (c *Context) Redirect(code int, location string) { + if c.Writer != nil { + c.Writer.Header().Set("Location", location) + c.Writer.WriteHeader(code) + } +} "# .to_owned() } @@ -1862,4 +2041,199 @@ mod tests { assert!(step.source.contains("\"fmt\"")); assert!(step.source.contains("\"os\"")); } + + // ── Phase 08 / 09 tier-(a) helpers + emitters ─────────────────────────── + + #[test] + fn rewrite_package_replaces_first_package_line() { + let src = "// header\npackage vuln\n\nimport \"net/http\"\n\nfunc Run() {}\n"; + let out = rewrite_package(src, "vulnentry"); + assert!( + out.contains("\npackage vulnentry\n"), + "rewrite must produce `package vulnentry`, got:\n{out}", + ); + assert!( + !out.contains("\npackage vuln\n"), + "original `package vuln` must be gone after rewrite, got:\n{out}", + ); + // Other lines preserved verbatim. + assert!(out.contains("// header")); + assert!(out.contains("import \"net/http\"")); + assert!(out.contains("func Run() {}")); + } + + #[test] + fn rewrite_package_handles_crlf_line_endings() { + let src = "package benign\r\nimport \"net/http\"\r\n"; + let out = rewrite_package(src, "vulnentry"); + assert!(out.starts_with("package vulnentry\r\n")); + assert!(out.contains("import \"net/http\"")); + } + + #[test] + fn rewrite_package_passes_through_when_no_package_line() { + let src = "// no package decl here\nimport \"net/http\"\n"; + let out = rewrite_package(src, "vulnentry"); + assert_eq!(out, src); + } + + #[test] + fn header_injection_tier_a_fires_when_net_http_imported() { + let mut spec = make_spec(PayloadSlot::Param(0)); + spec.entry_name = "Run".into(); + spec.expected_cap = Cap::HEADER_INJECTION; + spec.entry_file = "tests/dynamic_fixtures/header_injection/go/vuln.go".into(); + let harness = emit_header_injection_harness(&spec); + assert!( + harness.source.contains("nyx-harness/internal/vulnentry"), + "tier-(a) header_injection must import the rewritten fixture package", + ); + assert!( + harness.source.contains("nyxHeaderViaFixture(payload)"), + "tier-(a) header_injection must dispatch via fixture wrapper", + ); + assert!( + harness.source.contains("vulnentry.Run(rec, payload)"), + "tier-(a) header_injection must call .Run(rec, payload)", + ); + assert!( + harness.source.contains("rec.Header()"), + "tier-(a) header_injection must walk rec.Header() for captured headers", + ); + // Rewritten fixture must be staged under internal/vulnentry/. + let staged = harness + .extra_files + .iter() + .find(|(p, _)| p == "internal/vulnentry/vulnentry.go"); + assert!( + staged.is_some(), + "tier-(a) header_injection must stage internal/vulnentry/vulnentry.go", + ); + assert!( + staged.unwrap().1.contains("package vulnentry"), + "staged fixture must carry the rewritten package declaration", + ); + } + + #[test] + fn header_injection_tier_b_falls_back_when_no_net_http() { + let mut spec = make_spec(PayloadSlot::Param(0)); + spec.entry_name = "Run".into(); + spec.expected_cap = Cap::HEADER_INJECTION; + spec.entry_file = "/nonexistent/missing.go".into(); + let harness = emit_header_injection_harness(&spec); + assert!( + !harness.source.contains("nyx-harness/internal/vulnentry"), + "tier-(b) header_injection must not import a fixture package", + ); + assert!( + harness.source.contains("nyxHeaderProbe(\"Set-Cookie\", payload)"), + "tier-(b) header_injection must emit synthetic Set-Cookie probe", + ); + assert!( + harness + .extra_files + .iter() + .all(|(p, _)| p != "internal/vulnentry/vulnentry.go"), + "tier-(b) header_injection must not stage a rewritten fixture", + ); + } + + #[test] + fn open_redirect_tier_a_fires_when_gin_imported() { + let mut spec = make_spec(PayloadSlot::Param(0)); + spec.entry_name = "Run".into(); + spec.expected_cap = Cap::OPEN_REDIRECT; + spec.entry_file = "tests/dynamic_fixtures/open_redirect/go/vuln.go".into(); + let harness = emit_open_redirect_harness(&spec); + assert!( + harness.source.contains("nyx-harness/internal/vulnentry"), + "tier-(a) open_redirect must import the rewritten fixture package", + ); + assert!( + harness.source.contains("nyx-harness/internal/vulnentry/gin"), + "tier-(a) open_redirect must import the local gin stub", + ); + assert!( + harness.source.contains("nyxRedirectViaFixture(payload)"), + "tier-(a) open_redirect must dispatch via fixture wrapper", + ); + assert!( + harness.source.contains("vulnentry.Run(ctx, payload)"), + "tier-(a) open_redirect must call .Run(ctx, payload)", + ); + assert!( + harness.source.contains("rec.Header().Get(\"Location\")"), + "tier-(a) open_redirect must read Location off the recorder", + ); + let staged_fixture = harness + .extra_files + .iter() + .find(|(p, _)| p == "internal/vulnentry/vulnentry.go"); + assert!( + staged_fixture.is_some(), + "tier-(a) open_redirect must stage internal/vulnentry/vulnentry.go", + ); + let staged_fixture = staged_fixture.unwrap(); + assert!( + staged_fixture.1.contains("package vulnentry"), + "staged fixture must carry the rewritten package", + ); + assert!( + staged_fixture + .1 + .contains("\"nyx-harness/internal/vulnentry/gin\""), + "staged fixture must have its gin import rewritten to the local stub", + ); + let staged_gin = harness + .extra_files + .iter() + .find(|(p, _)| p == "internal/vulnentry/gin/gin.go"); + assert!( + staged_gin.is_some(), + "tier-(a) open_redirect must stage the gin stub", + ); + assert!( + staged_gin.unwrap().1.contains("func (c *Context) Redirect("), + "staged gin stub must expose Redirect", + ); + } + + #[test] + fn open_redirect_tier_b_falls_back_when_no_framework() { + let mut spec = make_spec(PayloadSlot::Param(0)); + spec.entry_name = "Run".into(); + spec.expected_cap = Cap::OPEN_REDIRECT; + spec.entry_file = "/nonexistent/missing.go".into(); + let harness = emit_open_redirect_harness(&spec); + assert!( + !harness.source.contains("nyx-harness/internal/vulnentry"), + "tier-(b) open_redirect must not import a fixture package", + ); + assert!( + harness + .source + .contains("nyxRedirectProbe(payload, requestHost)"), + "tier-(b) open_redirect must emit synthetic redirect probe", + ); + assert!( + harness + .extra_files + .iter() + .all(|(p, _)| !p.starts_with("internal/vulnentry/")), + "tier-(b) open_redirect must not stage any rewritten fixture or stub", + ); + } + + #[test] + fn gin_stub_pkg_exposes_redirect_method() { + let stub = gin_stub_pkg(); + assert!( + stub.contains("func (c *Context) Redirect(code int, location string)"), + "gin stub must expose a Redirect method tier-(a) open_redirect drives the fixture through", + ); + // The Redirect method must set Location and write the status. + assert!(stub.contains("c.Writer.Header().Set(\"Location\", location)")); + assert!(stub.contains("c.Writer.WriteHeader(code)")); + } }