mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-15 20:05:13 +02:00
[pitboss] phase 08: Track J.6 + Track L.6 — HEADER_INJECTION corpus + every HTTP framework
This commit is contained in:
parent
59d627cb22
commit
e0e49f65d3
45 changed files with 2552 additions and 41 deletions
|
|
@ -505,6 +505,14 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
return Ok(emit_xxe_harness(spec));
|
||||
}
|
||||
|
||||
// Phase 08 (Track J.6): HEADER_INJECTION-sink short-circuit. The
|
||||
// Go harness models `w.Header().Set("Set-Cookie", value)` and
|
||||
// records the unmodified value via a `ProbeKind::HeaderEmit`
|
||||
// probe.
|
||||
if spec.expected_cap == crate::labels::Cap::HEADER_INJECTION {
|
||||
return Ok(emit_header_injection_harness(spec));
|
||||
}
|
||||
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let shape = GoShape::detect(spec, &entry_source);
|
||||
let main_go = generate_main_go(spec, shape);
|
||||
|
|
@ -610,6 +618,68 @@ 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 {
|
||||
let shim = probe_shim();
|
||||
let go_mod = generate_go_mod();
|
||||
let source = format!(
|
||||
r##"// Nyx dynamic harness — HEADER_INJECTION http.ResponseWriter.Header().Set (Phase 08 / Track J.6).
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
{shim}
|
||||
|
||||
func nyxHeaderProbe(name, value string) {{
|
||||
__nyx_emit(map[string]interface{{}}{{
|
||||
"sink_callee": "http.ResponseWriter.Header.Set",
|
||||
"args": []map[string]interface{{}}{{
|
||||
{{"kind": "String", "value": name}},
|
||||
{{"kind": "String", "value": value}},
|
||||
}},
|
||||
"captured_at_ns": uint64(time.Now().UnixNano()),
|
||||
"payload_id": os.Getenv("NYX_PAYLOAD_ID"),
|
||||
"kind": map[string]interface{{}}{{"kind": "HeaderEmit", "name": name, "value": value}},
|
||||
"witness": __nyx_witness("http.ResponseWriter.Header.Set", []string{{name, value}}),
|
||||
}})
|
||||
}}
|
||||
|
||||
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}})
|
||||
fmt.Println(string(body))
|
||||
}}
|
||||
"##
|
||||
);
|
||||
HarnessSource {
|
||||
source,
|
||||
filename: "main.go".to_owned(),
|
||||
command: vec!["./nyx_harness".to_owned()],
|
||||
extra_files: vec![("go.mod".to_owned(), go_mod)],
|
||||
entry_subpath: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_main_go(spec: &HarnessSpec, shape: GoShape) -> String {
|
||||
let entry_fn = capitalize_first(&spec.entry_name);
|
||||
let pre_call = pre_call_setup(spec);
|
||||
|
|
|
|||
|
|
@ -567,6 +567,9 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
if spec.expected_cap == crate::labels::Cap::XPATH_INJECTION {
|
||||
return Ok(emit_xpath_harness(spec));
|
||||
}
|
||||
if spec.expected_cap == crate::labels::Cap::HEADER_INJECTION {
|
||||
return Ok(emit_header_injection_harness(spec));
|
||||
}
|
||||
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let shape = JavaShape::detect(spec, &entry_source);
|
||||
|
|
@ -1209,6 +1212,87 @@ public class NyxHarness {{
|
|||
}
|
||||
}
|
||||
|
||||
/// Phase 08 — Track J.6 header-injection harness for Java
|
||||
/// (`HttpServletResponse.setHeader`).
|
||||
///
|
||||
/// Reads `NYX_PAYLOAD`, calls a synthetic instrumented
|
||||
/// `response.setHeader("Set-Cookie", value)` 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 03 / 04 / 05 / 06 / 07.
|
||||
pub fn emit_header_injection_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
let source = format!(
|
||||
r#"// Nyx dynamic harness — HEADER_INJECTION HttpServletResponse.setHeader (Phase 08 / Track J.6).
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
|
||||
public class NyxHarness {{
|
||||
{shim}
|
||||
|
||||
static void nyxHeaderProbe(String name, String value) {{
|
||||
String p = System.getenv("NYX_PROBE_PATH");
|
||||
if (p == null || p.isEmpty()) return;
|
||||
long now = System.nanoTime();
|
||||
String pid = System.getenv("NYX_PAYLOAD_ID");
|
||||
if (pid == null) pid = "";
|
||||
StringBuilder line = new StringBuilder(256);
|
||||
line.append("{{\"sink_callee\":\"HttpServletResponse.setHeader\",\"args\":[");
|
||||
line.append("{{\"kind\":\"String\",\"value\":\"");
|
||||
nyxJsonEscape(name, line);
|
||||
line.append("\"}},{{\"kind\":\"String\",\"value\":\"");
|
||||
nyxJsonEscape(value, line);
|
||||
line.append("\"}}],");
|
||||
line.append("\"captured_at_ns\":").append(now).append(',');
|
||||
line.append("\"payload_id\":\"");
|
||||
nyxJsonEscape(pid, line);
|
||||
line.append("\",\"kind\":{{\"kind\":\"HeaderEmit\",\"name\":\"");
|
||||
nyxJsonEscape(name, line);
|
||||
line.append("\",\"value\":\"");
|
||||
nyxJsonEscape(value, line);
|
||||
line.append("\"}},");
|
||||
line.append("\"witness\":");
|
||||
line.append(nyxWitnessJson("HttpServletResponse.setHeader", new String[]{{name, value}}));
|
||||
line.append("}}\n");
|
||||
try (FileWriter fw = new FileWriter(p, true)) {{
|
||||
fw.write(line.toString());
|
||||
}} catch (IOException e) {{
|
||||
// best-effort
|
||||
}}
|
||||
}}
|
||||
|
||||
public static void main(String[] args) {{
|
||||
String payload = System.getenv("NYX_PAYLOAD");
|
||||
if (payload == null) payload = "";
|
||||
String name = "Set-Cookie";
|
||||
String value = payload;
|
||||
nyxHeaderProbe(name, value);
|
||||
System.out.println("__NYX_SINK_HIT__");
|
||||
StringBuilder body = new StringBuilder(64);
|
||||
body.append("{{\"name\":\"");
|
||||
nyxJsonEscape(name, body);
|
||||
body.append("\",\"value\":\"");
|
||||
nyxJsonEscape(value, body);
|
||||
body.append("\"}}");
|
||||
System.out.println(body.toString());
|
||||
}}
|
||||
}}
|
||||
"#
|
||||
);
|
||||
HarnessSource {
|
||||
source,
|
||||
filename: "NyxHarness.java".to_owned(),
|
||||
command: vec![
|
||||
"java".to_owned(),
|
||||
"-cp".to_owned(),
|
||||
".".to_owned(),
|
||||
"NyxHarness".to_owned(),
|
||||
],
|
||||
extra_files: Vec::new(),
|
||||
entry_subpath: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Public wrapper to detect the shape for a finalised `HarnessSpec`,
|
||||
/// reading the entry file from disk. Exposed so test helpers can pin a
|
||||
/// per-fixture shape without round-tripping through [`emit`].
|
||||
|
|
|
|||
|
|
@ -449,6 +449,14 @@ pub fn emit(spec: &HarnessSpec, is_typescript: bool) -> Result<HarnessSource, Un
|
|||
return Ok(emit_xpath_harness(spec));
|
||||
}
|
||||
|
||||
// Phase 08 (Track J.6): HEADER_INJECTION-sink short-circuit. The
|
||||
// synthetic harness calls an instrumented `res.setHeader` shim
|
||||
// that records the unmodified value bytes via a
|
||||
// `ProbeKind::HeaderEmit` probe.
|
||||
if spec.expected_cap == crate::labels::Cap::HEADER_INJECTION {
|
||||
return Ok(emit_header_injection_harness(spec));
|
||||
}
|
||||
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let shape = JsShape::detect(spec, &entry_source);
|
||||
let entry_subpath = entry_subpath_for_shape(shape, is_typescript);
|
||||
|
|
@ -610,6 +618,58 @@ console.log(JSON.stringify({{ expr: expr, nodes_returned: nodes }}));
|
|||
}
|
||||
}
|
||||
|
||||
/// Phase 08 — Track J.6 header-injection harness for Node
|
||||
/// (`http.ServerResponse#setHeader`).
|
||||
///
|
||||
/// Reads `NYX_PAYLOAD`, calls a synthetic instrumented
|
||||
/// `res.setHeader('Set-Cookie', value)` 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 03 / 04 / 05 / 06 / 07.
|
||||
pub fn emit_header_injection_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
let body = format!(
|
||||
r#"// Nyx dynamic harness — HEADER_INJECTION http.ServerResponse#setHeader (Phase 08 / Track J.6).
|
||||
{shim}
|
||||
|
||||
function nyxHeaderProbe(name, value) {{
|
||||
const p = process.env.NYX_PROBE_PATH;
|
||||
if (!p) return;
|
||||
const rec = {{
|
||||
sink_callee: 'http.ServerResponse#setHeader',
|
||||
args: [
|
||||
{{ kind: 'String', value: name }},
|
||||
{{ kind: 'String', value: value }},
|
||||
],
|
||||
captured_at_ns: Number(process.hrtime.bigint()),
|
||||
payload_id: process.env.NYX_PAYLOAD_ID || '',
|
||||
kind: {{ kind: 'HeaderEmit', name: name, value: value }},
|
||||
witness: __nyx_witness('http.ServerResponse#setHeader', [name, value]),
|
||||
}};
|
||||
try {{
|
||||
require('fs').appendFileSync(p, JSON.stringify(rec) + '\n');
|
||||
}} catch (e) {{
|
||||
// best-effort
|
||||
}}
|
||||
}}
|
||||
|
||||
const payload = process.env.NYX_PAYLOAD || '';
|
||||
const name = 'Set-Cookie';
|
||||
const value = payload;
|
||||
nyxHeaderProbe(name, value);
|
||||
console.log('__NYX_SINK_HIT__');
|
||||
console.log(JSON.stringify({{ name: name, value: value }}));
|
||||
"#
|
||||
);
|
||||
HarnessSource {
|
||||
source: body,
|
||||
filename: "harness.js".to_owned(),
|
||||
command: vec!["node".to_owned(), "harness.js".to_owned()],
|
||||
extra_files: Vec::new(),
|
||||
entry_subpath: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 26 — Node chain-step harness (shared between JS + TS emitters).
|
||||
///
|
||||
/// Splices the Node probe shim ([`probe_shim`]) in front of a minimal
|
||||
|
|
|
|||
|
|
@ -432,6 +432,10 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
if spec.expected_cap == crate::labels::Cap::XPATH_INJECTION {
|
||||
return Ok(emit_xpath_harness(spec));
|
||||
}
|
||||
// Phase 08 (Track J.6): HEADER_INJECTION-sink short-circuit.
|
||||
if spec.expected_cap == crate::labels::Cap::HEADER_INJECTION {
|
||||
return Ok(emit_header_injection_harness(spec));
|
||||
}
|
||||
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let shape = PhpShape::detect(spec, &entry_source);
|
||||
|
|
@ -869,6 +873,54 @@ echo json_encode(['expr' => $expr, 'nodes_returned' => $nodes]) . "\n";
|
|||
}
|
||||
}
|
||||
|
||||
/// Phase 08 — Track J.6 header-injection harness for PHP (`header()`).
|
||||
///
|
||||
/// Reads `NYX_PAYLOAD`, calls a synthetic instrumented `header()`
|
||||
/// 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 03 / 04 / 05 / 06 /
|
||||
/// 07.
|
||||
pub fn emit_header_injection_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
let body = format!(
|
||||
r#"<?php
|
||||
// Nyx dynamic harness — HEADER_INJECTION header() (Phase 08 / Track J.6).
|
||||
{shim}
|
||||
|
||||
function _nyx_header_probe(string $name, string $value): void {{
|
||||
$p = getenv('NYX_PROBE_PATH');
|
||||
if ($p === false || $p === '') return;
|
||||
$rec = [
|
||||
'sink_callee' => 'header()',
|
||||
'args' => [
|
||||
['kind' => 'String', 'value' => $name],
|
||||
['kind' => 'String', 'value' => $value],
|
||||
],
|
||||
'captured_at_ns' => (int) hrtime(true),
|
||||
'payload_id' => (string) (getenv('NYX_PAYLOAD_ID') ?: ''),
|
||||
'kind' => ['kind' => 'HeaderEmit', 'name' => $name, 'value' => $value],
|
||||
'witness' => __nyx_witness('header()', [$name, $value]),
|
||||
];
|
||||
@file_put_contents($p, json_encode($rec) . "\n", FILE_APPEND);
|
||||
}}
|
||||
|
||||
$payload = (string) (getenv('NYX_PAYLOAD') ?: '');
|
||||
$name = 'Set-Cookie';
|
||||
$value = $payload;
|
||||
_nyx_header_probe($name, $value);
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
echo json_encode(['name' => $name, 'value' => $value]) . "\n";
|
||||
"#
|
||||
);
|
||||
HarnessSource {
|
||||
source: body,
|
||||
filename: "harness.php".to_owned(),
|
||||
command: vec!["php".to_owned(), "harness.php".to_owned()],
|
||||
extra_files: Vec::new(),
|
||||
entry_subpath: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_source(spec: &HarnessSpec, shape: PhpShape) -> String {
|
||||
let entry_fn = &spec.entry_name;
|
||||
let pre_call = build_pre_call(spec, shape);
|
||||
|
|
|
|||
|
|
@ -640,6 +640,16 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
return Ok(emit_xpath_harness(spec));
|
||||
}
|
||||
|
||||
// Phase 08 (Track J.6): short-circuit to the header-injection
|
||||
// harness when the spec's expected cap is HEADER_INJECTION. The
|
||||
// harness splices the payload into a synthetic
|
||||
// `flask.Response.headers["Set-Cookie"] = value` assignment and
|
||||
// records the unescaped value via a `ProbeKind::HeaderEmit`
|
||||
// probe consumed by the `HeaderInjected` oracle.
|
||||
if spec.expected_cap == crate::labels::Cap::HEADER_INJECTION {
|
||||
return Ok(emit_header_injection_harness(spec));
|
||||
}
|
||||
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let shape = PythonShape::detect(spec, &entry_source);
|
||||
let body = generate_for_shape(spec, shape);
|
||||
|
|
@ -1085,6 +1095,74 @@ if __name__ == "__main__":
|
|||
}
|
||||
}
|
||||
|
||||
/// Phase 08 — Track J.6 header-injection harness for Python (Flask
|
||||
/// `Response.headers.__setitem__`).
|
||||
///
|
||||
/// Reads `NYX_PAYLOAD`, calls a synthetic instrumented
|
||||
/// `flask.Response.headers["Set-Cookie"] = value` assignment that
|
||||
/// records the *unmodified* value bytes (including any embedded
|
||||
/// `\r\n`) via a `ProbeKind::HeaderEmit` probe. A vuln payload
|
||||
/// carrying raw CRLF trips the
|
||||
/// [`crate::dynamic::oracle::ProbePredicate::HeaderInjected`]
|
||||
/// oracle; the paired benign control passes the same logical bytes
|
||||
/// pre-encoded via `urllib.parse.quote`, so the captured value
|
||||
/// carries `%0D%0A` (not the raw bytes) and the predicate stays
|
||||
/// clear.
|
||||
pub fn emit_header_injection_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||
let probe = probe_shim();
|
||||
let body = format!(
|
||||
r#"#!/usr/bin/env python3
|
||||
"""Nyx dynamic harness — HEADER_INJECTION flask.Response.headers.__setitem__ (Phase 08 / Track J.6)."""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
{probe}
|
||||
|
||||
|
||||
def _nyx_header_probe(name, value):
|
||||
rec = {{
|
||||
"sink_callee": "flask.Response.headers.__setitem__",
|
||||
"args": [
|
||||
{{"kind": "String", "value": name}},
|
||||
{{"kind": "String", "value": value}},
|
||||
],
|
||||
"captured_at_ns": time.time_ns(),
|
||||
"payload_id": os.environ.get("NYX_PAYLOAD_ID", ""),
|
||||
"kind": {{"kind": "HeaderEmit", "name": name, "value": value}},
|
||||
"witness": __nyx_witness("flask.Response.headers.__setitem__", [name, value]),
|
||||
}}
|
||||
__nyx_emit(rec)
|
||||
|
||||
|
||||
def _nyx_run():
|
||||
payload = os.environ.get("NYX_PAYLOAD", "")
|
||||
# Synthetic instrumented setter — mirrors
|
||||
# `werkzeug.datastructures.Headers.__setitem__` semantics: the
|
||||
# value bytes flow through unmodified, so a tainted payload that
|
||||
# carries raw `\r\n` lands on the wire as a header split.
|
||||
name = "Set-Cookie"
|
||||
value = payload
|
||||
_nyx_header_probe(name, value)
|
||||
print("__NYX_SINK_HIT__", flush=True)
|
||||
sys.stdout.write(json.dumps({{"name": name, "value": value}}) + "\n")
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/// Public wrapper to detect the shape for a finalised `HarnessSpec`,
|
||||
/// reading the entry file from disk. Exposed so test helpers can pin a
|
||||
/// per-fixture shape without round-tripping through [`emit`].
|
||||
|
|
|
|||
|
|
@ -424,6 +424,9 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
if spec.expected_cap == crate::labels::Cap::XXE {
|
||||
return Ok(emit_xxe_harness(spec));
|
||||
}
|
||||
if spec.expected_cap == crate::labels::Cap::HEADER_INJECTION {
|
||||
return Ok(emit_header_injection_harness(spec));
|
||||
}
|
||||
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let shape = RubyShape::detect(spec, &entry_source);
|
||||
|
|
@ -616,6 +619,57 @@ STDOUT.flush
|
|||
}
|
||||
}
|
||||
|
||||
/// Phase 08 — Track J.6 header-injection harness for Ruby
|
||||
/// (`Rack::Response#set_header`).
|
||||
///
|
||||
/// Reads `NYX_PAYLOAD`, calls a synthetic instrumented
|
||||
/// `response.set_header('Set-Cookie', value)` 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 03 / 04 / 05.
|
||||
pub fn emit_header_injection_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
let body = format!(
|
||||
r#"# Nyx dynamic harness — HEADER_INJECTION Rack::Response#set_header (Phase 08 / Track J.6).
|
||||
require 'json'
|
||||
|
||||
{shim}
|
||||
|
||||
def _nyx_header_probe(name, value)
|
||||
p = ENV['NYX_PROBE_PATH']
|
||||
return if p.nil? || p.empty?
|
||||
rec = {{
|
||||
'sink_callee' => 'Rack::Response#set_header',
|
||||
'args' => [
|
||||
{{ 'kind' => 'String', 'value' => name }},
|
||||
{{ 'kind' => 'String', 'value' => value }},
|
||||
],
|
||||
'captured_at_ns' => Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond),
|
||||
'payload_id' => ENV['NYX_PAYLOAD_ID'] || '',
|
||||
'kind' => {{ 'kind' => 'HeaderEmit', 'name' => name, 'value' => value }},
|
||||
'witness' => __nyx_witness('Rack::Response#set_header', [name, value]),
|
||||
}}
|
||||
File.open(p, 'a') {{ |f| f.write(rec.to_json + "\n") }}
|
||||
end
|
||||
|
||||
payload = ENV['NYX_PAYLOAD'] || ''
|
||||
name = 'Set-Cookie'
|
||||
value = payload
|
||||
_nyx_header_probe(name, value)
|
||||
STDOUT.puts '__NYX_SINK_HIT__'
|
||||
STDOUT.puts JSON.generate({{ 'name' => name, 'value' => value }})
|
||||
STDOUT.flush
|
||||
"#
|
||||
);
|
||||
HarnessSource {
|
||||
source: body,
|
||||
filename: "harness.rb".to_owned(),
|
||||
command: vec!["ruby".to_owned(), "harness.rb".to_owned()],
|
||||
extra_files: vec![],
|
||||
entry_subpath: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_source(spec: &HarnessSpec, shape: RubyShape) -> String {
|
||||
let entry_fn = &spec.entry_name;
|
||||
let pre_call = build_pre_call(spec);
|
||||
|
|
|
|||
|
|
@ -557,6 +557,96 @@ pub fn detect_shape(spec: &HarnessSpec) -> RustShape {
|
|||
RustShape::detect(spec, &src)
|
||||
}
|
||||
|
||||
/// Phase 08 — Track J.6 header-injection harness for Rust
|
||||
/// (`axum`-style `HeaderMap::insert`).
|
||||
///
|
||||
/// Reads `NYX_PAYLOAD`, calls a synthetic instrumented
|
||||
/// `headers_mut().insert("Set-Cookie", value)` shim that records the
|
||||
/// *unmodified* value bytes (including any embedded `\r\n`) via a
|
||||
/// `ProbeKind::HeaderEmit` probe. Std-only — no `Cargo.toml`
|
||||
/// dependencies beyond the always-pinned `libc` (used by the probe
|
||||
/// shim's crash guard).
|
||||
pub fn emit_header_injection_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
let cargo_toml = generate_cargo_toml(Cap::HEADER_INJECTION);
|
||||
let main_rs = format!(
|
||||
r##"//! Nyx dynamic harness — HEADER_INJECTION HeaderMap::insert (Phase 08 / Track J.6).
|
||||
use std::env;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::time::{{SystemTime, UNIX_EPOCH}};
|
||||
|
||||
{shim}
|
||||
|
||||
fn nyx_json_escape(s: &str) -> String {{
|
||||
let mut out = String::with_capacity(s.len() + 2);
|
||||
for c in s.chars() {{
|
||||
match c {{
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
c if (c as u32) < 0x20 => {{
|
||||
out.push_str(&format!("\\u{{:04x}}", c as u32));
|
||||
}}
|
||||
c => out.push(c),
|
||||
}}
|
||||
}}
|
||||
out
|
||||
}}
|
||||
|
||||
fn nyx_header_probe(name: &str, value: &str) {{
|
||||
let p = match env::var("NYX_PROBE_PATH") {{ Ok(s) => s, Err(_) => return }};
|
||||
if p.is_empty() {{ return; }}
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos() as u64).unwrap_or(0);
|
||||
let pid = env::var("NYX_PAYLOAD_ID").unwrap_or_default();
|
||||
let mut line = String::new();
|
||||
line.push_str("{{\"sink_callee\":\"HeaderMap::insert\",\"args\":[");
|
||||
line.push_str("{{\"kind\":\"String\",\"value\":\"");
|
||||
line.push_str(&nyx_json_escape(name));
|
||||
line.push_str("\"}},{{\"kind\":\"String\",\"value\":\"");
|
||||
line.push_str(&nyx_json_escape(value));
|
||||
line.push_str("\"}}],");
|
||||
line.push_str("\"captured_at_ns\":");
|
||||
line.push_str(&now.to_string());
|
||||
line.push_str(",\"payload_id\":\"");
|
||||
line.push_str(&nyx_json_escape(&pid));
|
||||
line.push_str("\",\"kind\":{{\"kind\":\"HeaderEmit\",\"name\":\"");
|
||||
line.push_str(&nyx_json_escape(name));
|
||||
line.push_str("\",\"value\":\"");
|
||||
line.push_str(&nyx_json_escape(value));
|
||||
line.push_str("\"}},\"witness\":{{}}}}\n");
|
||||
if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(&p) {{
|
||||
let _ = f.write_all(line.as_bytes());
|
||||
}}
|
||||
}}
|
||||
|
||||
fn main() {{
|
||||
let payload = env::var("NYX_PAYLOAD").unwrap_or_default();
|
||||
let name = "Set-Cookie";
|
||||
let value = &payload;
|
||||
nyx_header_probe(name, value);
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let mut body = String::new();
|
||||
body.push_str("{{\"name\":\"");
|
||||
body.push_str(&nyx_json_escape(name));
|
||||
body.push_str("\",\"value\":\"");
|
||||
body.push_str(&nyx_json_escape(value));
|
||||
body.push_str("\"}}");
|
||||
println!("{{body}}", body = body);
|
||||
}}
|
||||
"##
|
||||
);
|
||||
HarnessSource {
|
||||
source: main_rs,
|
||||
filename: "src/main.rs".into(),
|
||||
command: vec!["target/release/nyx_harness".into()],
|
||||
extra_files: vec![("Cargo.toml".into(), cargo_toml)],
|
||||
entry_subpath: Some("src/entry.rs".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_entry_source(entry_file: &str) -> String {
|
||||
let candidates = [PathBuf::from(entry_file), PathBuf::from(".").join(entry_file)];
|
||||
for path in &candidates {
|
||||
|
|
@ -569,6 +659,14 @@ fn read_entry_source(entry_file: &str) -> String {
|
|||
|
||||
/// Emit a Rust harness for `spec`.
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
// Phase 08 (Track J.6): HEADER_INJECTION-sink short-circuit. The
|
||||
// Rust harness models an `axum`-style `HeaderMap::insert` shim
|
||||
// that records the *unmodified* value bytes via a
|
||||
// `ProbeKind::HeaderEmit` probe.
|
||||
if spec.expected_cap == crate::labels::Cap::HEADER_INJECTION {
|
||||
return Ok(emit_header_injection_harness(spec));
|
||||
}
|
||||
|
||||
let shape = detect_shape(spec);
|
||||
|
||||
// Generic + LibfuzzerTarget accept Param(0)/EnvVar; richer shapes
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue