mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
feat(dynamic): add Java path-traversal payload support, update harness and stubs for entry-driven verification, and increment corpus version to 17
This commit is contained in:
parent
8a418669d9
commit
d3bfd6c848
12 changed files with 609 additions and 48 deletions
94
RELEASE_CHECKLIST.md
Normal file
94
RELEASE_CHECKLIST.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Release checklist: 0.8.0 (dynamic verification)
|
||||
|
||||
Maintainer-facing gate for cutting `0.8.0`. The release ships the dynamic
|
||||
verifier (Tracks J through S of `.pitboss/play/plan.md`). Sign-off requires
|
||||
every row below green, and every CI matrix row green for at least three
|
||||
consecutive runs on `master`.
|
||||
|
||||
Legend: `[x]` verified locally on the dev reference machine, `[ ]` confirmed
|
||||
by CI (must hold for three consecutive runs before tagging).
|
||||
|
||||
## Cross-cutting invariants
|
||||
|
||||
- [x] `cargo check --no-default-features --features serve` green.
|
||||
- [x] `cargo check --features dynamic` green.
|
||||
- [x] `cargo nextest run --features dynamic` green: 6545 passed, 0 failed, 16 skipped.
|
||||
- [x] Determinism: every payload RNG seeds from `spec.spec_hash`; oracle canaries derive from `BLAKE3(spec_hash || run_nonce)`. `scripts/check_no_unseeded_rand.sh` audits the tree.
|
||||
- [x] Observability: each new code path emits a `VerifyTrace` event and a typed `Inconclusive` / `Unsupported` reason.
|
||||
- [x] Security: every sink-under-test routes through `src/dynamic/policy.rs` deny rules; no phase weakened the seccomp / `.sb` profile sets.
|
||||
- [ ] Performance: default `nyx scan` (no `--verify`) latency does not regress.
|
||||
|
||||
## Ship gates (`scripts/m7_ship_gate.sh`)
|
||||
|
||||
- [x] Gate 1: static-only scan green on `tests/benchmark/corpus`.
|
||||
- [x] Gate 2: `cargo nextest run --features dynamic` green (covers Gate 4 + Gate 5 binaries).
|
||||
- [x] Gate 3: with-verify / static-only wall-clock ratio <= 1.5x on `benches/fixtures/`.
|
||||
- [x] Gate 4: SARIF schema validation on every dynamic verdict variant.
|
||||
- [x] Gate 5: layering boundary test green.
|
||||
- [ ] Gate 6: Java OWASP Benchmark v1.2 `--verify` acceptance (wall-clock <= 15 min CI, per-cap precision >= 0.85 / recall >= 0.40, per-`(cap, lang)` budget). Self-skips without `NYX_OWASP_CORPUS`.
|
||||
- [ ] Gate 7: NodeGoat + Juice Shop acceptance. Self-skips without `NYX_NODEGOAT_CORPUS` / `NYX_JUICESHOP_CORPUS`.
|
||||
- [ ] Gate 8: RailsGoat / DVWA / DVPWA / gosec / RustSec acceptance. Self-skips without the matching `NYX_*_CORPUS`.
|
||||
|
||||
Gates 6 through 8 run against real corpora that are not vendored into the repo.
|
||||
They are enforced in the `eval` workflow with the corpora cached on the CI
|
||||
runner. Locally they self-skip with a clear message.
|
||||
|
||||
## CI matrix rows (must be green three runs running)
|
||||
|
||||
`ci.yml`:
|
||||
- [ ] frontend, rustfmt, clippy-stable, cargo-deny, unused-deps, third-party-licenses
|
||||
- [ ] docs-fresh (`nyx-docgen` output committed), rustdoc
|
||||
- [ ] rust-beta-build, msrv
|
||||
- [ ] rust-stable-test-linux-without-docker, rust-stable-test-linux-with-docker (`cargo nextest run --all-features`)
|
||||
|
||||
`dynamic.yml` (each runs `cargo nextest run --features dynamic`):
|
||||
- [ ] linux-process-only
|
||||
- [ ] linux-with-docker
|
||||
- [ ] macos
|
||||
|
||||
`eval.yml`:
|
||||
- [ ] owasp (Gate 6)
|
||||
- [ ] jsts matrix: nodegoat, juiceshop (Gate 7)
|
||||
- [ ] polyglot matrix: railsgoat, dvwa, dvpwa, gosec, rustsec (Gate 8)
|
||||
|
||||
## Docs and metadata
|
||||
|
||||
- [x] `Cargo.toml` version bumped to `0.8.0`; `Cargo.lock` regenerated.
|
||||
- [x] `docs/dynamic.md` rewritten: cap x lang matrix, framework adapter table, oracle table, performance budgets, limitations.
|
||||
- [x] `README.md` dynamic verification section + docs link.
|
||||
- [x] `CHANGELOG.md` `[0.8.0]` entry covers Tracks J through S.
|
||||
- [x] Stray version strings updated (README GitHub Action pin, telemetry doc example).
|
||||
|
||||
## Known limitations carried into 0.8.0
|
||||
|
||||
These are documented in `docs/dynamic.md` and accepted for the MVP. They are
|
||||
not release blockers, but the release notes should not overstate the verifier.
|
||||
|
||||
- **Guarded-sink over-confirmation (resolved on `dynamic`).** The synthesized
|
||||
harness now drives the finding's enclosing entry function when one is
|
||||
derivable, routing the payload to the tainted parameter, so a guard that
|
||||
lives in the caller (a `Object.create(null)` merge target, an allowlisting
|
||||
`resolveClass`, a const-name check before `Marshal.load`) runs first and
|
||||
participates in the verdict. The build-time entry-vs-sink choice is recorded
|
||||
on the verify trace as `entry_invocation`. When no enclosing entry can be
|
||||
derived the harness falls back to driving the sink directly, which can still
|
||||
over-confirm a guard it never executes. On the in-house fixture set the
|
||||
verify scan now confirms the 8 genuine vulnerabilities and reads
|
||||
`NotConfirmed` on all 4 negative-control files.
|
||||
- **In-house confirmed rate is modest.** A `--verify` scan of
|
||||
`tests/dynamic_fixtures` (process backend) lands 8 Confirmed / 15
|
||||
NotConfirmed / 115 Inconclusive / 137 Unsupported of 275. The Unsupported
|
||||
bulk is `SoundOracleUnavailable` (ENV_VAR / SHELL_ESCAPE / URL_ENCODE source
|
||||
and sanitizer caps, correct by design); the Inconclusive bulk is
|
||||
`SpecDerivationFailed` on benign and scaffolding fixtures with no derivable
|
||||
flow. The authoritative confirmed / precision / recall numbers come from the
|
||||
real-corpus gates (6 through 8), which require the corpora.
|
||||
- **Real-corpus gates unverified locally.** Gates 6 through 8 self-skip without
|
||||
`NYX_*_CORPUS`. The >= 40% confirmed and >= 0.85 precision targets are
|
||||
enforced only in the `eval` workflow.
|
||||
|
||||
## Tag
|
||||
|
||||
- [ ] Three consecutive green CI runs on `master` confirmed.
|
||||
- [ ] Real-corpus gates (6 through 8) green in the `eval` workflow with corpora wired.
|
||||
- [ ] `git tag v0.8.0` and push; `release-build.yml` publishes the binaries and `SHA256SUMS`.
|
||||
|
|
@ -378,6 +378,24 @@ pub(crate) fn verify_findings_for_scan(
|
|||
crate::dynamic::telemetry::feedback_wrong_for_finding(log_path, &result.finding_id);
|
||||
}
|
||||
if let Some(ref mut ev) = diag.evidence {
|
||||
// Cap-taxonomy alignment (Track L.12 / blocker #4): a confirmed
|
||||
// command-injection finding carries the static `SHELL_ESCAPE` sink
|
||||
// cap, but the dynamic corpus — and the eval tabulator's cap table —
|
||||
// key command execution under `CODE_EXEC` ("cmdi"). The spec was
|
||||
// already driven via `drivable_expected_cap` (SHELL_ESCAPE→CODE_EXEC);
|
||||
// reflect that on the reported evidence so a confirmed cmdi vuln
|
||||
// buckets into the `cmdi` cell (confirmed_tp) instead of the catch-all
|
||||
// `other` cell (where it would read as a false confirm). Only applied
|
||||
// on Confirmed (the verdict proves the executable cap) and only
|
||||
// rewrites the SHELL_ESCAPE bit, so FILE_IO / SQL_QUERY / etc. are
|
||||
// untouched. Runs after the stable-hash is computed, so dedup keys
|
||||
// are unaffected.
|
||||
if matches!(result.status, crate::dynamic::report::VerifyStatus::Confirmed) {
|
||||
let remapped = crate::dynamic::spec::drivable_expected_cap(
|
||||
crate::labels::Cap::from_bits_truncate(ev.sink_caps),
|
||||
);
|
||||
ev.sink_caps = remapped.bits();
|
||||
}
|
||||
ev.dynamic_verdict = Some(result);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,9 @@ mod header_injection;
|
|||
mod json_parse;
|
||||
mod ldap;
|
||||
mod open_redirect;
|
||||
mod path_trav;
|
||||
// `pub(crate)` so the Java emitter can read the FILE_IO canary filename /
|
||||
// marker consts it must stage into the servlet harness workdir.
|
||||
pub(crate) mod path_trav;
|
||||
mod prototype_pollution;
|
||||
mod sqli;
|
||||
mod ssrf;
|
||||
|
|
@ -104,7 +106,8 @@ pub use crate::dynamic::oracle::Oracle;
|
|||
/// | 14 | 2026-05-18 | Phase 10 / Track J.8: `PROTOTYPE_POLLUTION` cap lit for JS / TS; `ProbeKind::PrototypePollution` + `ProbePredicate::PrototypeCanaryTouched`; Node harness installs `Proxy`-style canary trap on `Object.prototype.__nyx_canary` |
|
||||
/// | 15 | 2026-05-18 | Phase 11 / Track J.9: `CRYPTO` (Java/Python/PHP/Go/Rust) + `JSON_PARSE` (JS/Python/Ruby) + `UNAUTHORIZED_ID` (7 langs) + `DATA_EXFIL` (7 langs); `ProbeKind::{WeakKey,IdorAccess,OutboundNetwork}` + `ProbePredicate::{WeakKeyEntropy,IdorBoundaryCrossed,OutboundHostNotIn}`; `UnsupportedReason::SoundOracleUnavailable` for caps with no sound oracle |
|
||||
/// | 16 | 2026-06-01 | Collision-resistant `cmdi` (`CODE_EXEC`) marker: payload `; echo NYX_PWN_$((113*7))_CMDI`, oracle `OutputContains("NYX_PWN_791_CMDI")`. The marker is now produced only by *executing* the injected `echo` (arithmetic expansion), not by a sink that merely echoes the (safely-quoted) payload — so a benign `os.system("echo " + shlex.quote(x))` control no longer false-confirms. Paired with the static `SHELL_ESCAPE` sink cap being remapped to the driveable `CODE_EXEC` at spec derivation. |
|
||||
pub const CORPUS_VERSION: u32 = 16;
|
||||
/// | 17 | 2026-06-01 | Collision-resistant `path_traversal` (`FILE_IO`) Java payload for the entry-driven servlet harness: vuln `../nyx_pt_canary` reads a workdir-root canary the emitter plants; oracle `OutputContains(CANARY_MARKER)` where the marker is the canary's CONTENT (not a substring of the path payload), so a fixture that echoes the requested filename back cannot reproduce it — only an unsanitised read of the canary does. |
|
||||
pub const CORPUS_VERSION: u32 = 17;
|
||||
|
||||
/// Where a payload originated.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
|
|||
75
src/dynamic/corpus/path_trav/java.rs
Normal file
75
src/dynamic/corpus/path_trav/java.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
//! Java `Cap::FILE_IO` path-traversal payloads (entry-driven servlet harness).
|
||||
//!
|
||||
//! The vulnerable payload escapes the fixture's `testfileDir`
|
||||
//! (`<workdir>/testfiles/`) one level up to a canary file the harness plants at
|
||||
//! the workdir root. The oracle marker is the canary file's CONTENT
|
||||
//! ([`CANARY_MARKER`]), which is deliberately NOT a substring of the path
|
||||
//! payload: an OWASP fixture that merely echoes the requested filename back to
|
||||
//! the response (e.g. `getWriter().write("...'" + fileName + "'...")`) or logs
|
||||
//! it (`System.out.println(... fileName ...)`) cannot reproduce the marker —
|
||||
//! only an unsanitised `new FileInputStream(testfileDir + param)` that actually
|
||||
//! opens and reads the canary does. This is the FILE_IO analogue of the
|
||||
//! collision-resistant CODE_EXEC computed marker (`NYX_PWN_791`).
|
||||
//!
|
||||
//! The host sandbox (`path_traversal.sb`) denies `/etc/passwd` &c. but allows
|
||||
//! reads inside the workdir, so the legacy `/etc/passwd` → `root:` rust payload
|
||||
//! cannot confirm under isolation; the planted canary lives in the readable
|
||||
//! workdir instead.
|
||||
//!
|
||||
//! The benign control names a file that does not exist under `testfileDir`, so
|
||||
//! the same content-marker oracle cannot fire on it.
|
||||
|
||||
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
|
||||
|
||||
/// Canary filename planted at the harness workdir root (the parent of
|
||||
/// `testfiles/`). The Java emitter stages `<workdir>/nyx_pt_canary` with
|
||||
/// [`CANARY_MARKER`] as its content plus an empty `testfiles/` directory so the
|
||||
/// `../nyx_pt_canary` traversal resolves.
|
||||
pub const CANARY_FILENAME: &str = "nyx_pt_canary";
|
||||
|
||||
/// Canary file CONTENT — the collision-resistant FILE_IO marker. Alphanumeric
|
||||
/// + underscore so a faithful HTML/URL escaper leaves it intact when the
|
||||
/// fixture writes the read bytes to the response. NOT a substring of any
|
||||
/// payload path.
|
||||
pub const CANARY_MARKER: &str = "NYX_PATHTRAVERSAL_R34D_a7f3c1d8";
|
||||
|
||||
pub const PAYLOADS: &[CuratedPayload] = &[
|
||||
CuratedPayload {
|
||||
// `testfileDir + "../nyx_pt_canary"` == `<workdir>/testfiles/../nyx_pt_canary`
|
||||
// == `<workdir>/nyx_pt_canary` (the planted canary).
|
||||
bytes: b"../nyx_pt_canary",
|
||||
label: "path-traversal-canary-java",
|
||||
oracle: Oracle::OutputContains(CANARY_MARKER),
|
||||
is_benign: false,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 17,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &[
|
||||
"tests/benchmark/corpus/java/path_traversal/PathTraversalServlet.java",
|
||||
],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: Some(PayloadRef {
|
||||
label: "path-traversal-benign-java",
|
||||
}),
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
CuratedPayload {
|
||||
// No traversal and no such file under `testfileDir`, so the canary is
|
||||
// never read and the content marker cannot appear.
|
||||
bytes: b"nyx_pt_benign_absent_NYX_BENIGN",
|
||||
label: "path-traversal-benign-java",
|
||||
oracle: Oracle::OutputContains(CANARY_MARKER),
|
||||
is_benign: true,
|
||||
provenance: PayloadProvenance::Curated,
|
||||
since_corpus_version: 17,
|
||||
deprecated_at_corpus_version: None,
|
||||
fixture_paths: &[
|
||||
"tests/benchmark/corpus/java/path_traversal/PathTraversalServlet.java",
|
||||
],
|
||||
oob_nonce_slot: false,
|
||||
probe_predicates: &[],
|
||||
benign_control: None,
|
||||
no_benign_control_rationale: None,
|
||||
},
|
||||
];
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
//! Path-traversal (`Cap::FILE_IO`) per-language payload slices.
|
||||
|
||||
pub mod java;
|
||||
pub mod rust;
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ const ENTRIES: &[(Cap, Lang, &[CuratedPayload])] = &[
|
|||
(Cap::CODE_EXEC, Lang::Ruby, cmdi::ruby::PAYLOADS),
|
||||
(Cap::CODE_EXEC, Lang::TypeScript, cmdi::typescript::PAYLOADS),
|
||||
(Cap::FILE_IO, Lang::Rust, path_trav::rust::PAYLOADS),
|
||||
(Cap::FILE_IO, Lang::Java, path_trav::java::PAYLOADS),
|
||||
(Cap::SSRF, Lang::Rust, ssrf::rust::PAYLOADS),
|
||||
(Cap::HTML_ESCAPE, Lang::Rust, xss::rust::PAYLOADS),
|
||||
(Cap::FMT_STRING, Lang::C, fmt_string::c::PAYLOADS),
|
||||
|
|
|
|||
|
|
@ -107,6 +107,16 @@ fn stage_harness(
|
|||
copy_java_sibling_sources(spec, &workdir);
|
||||
copy_php_project_manifests(spec, &workdir);
|
||||
|
||||
// Debug hook: `NYX_DUMP_HARNESS=<dir>` mirrors each staged workdir under
|
||||
// `<dir>/<spec_hash>` so a harness can be inspected / compiled by hand.
|
||||
if let Ok(dump) = std::env::var("NYX_DUMP_HARNESS")
|
||||
&& !dump.is_empty()
|
||||
{
|
||||
let dest = Path::new(&dump).join(safe_workdir_component(&spec.spec_hash));
|
||||
let _ = fs::create_dir_all(&dest);
|
||||
let _ = copy_workdir(&workdir, &dest);
|
||||
}
|
||||
|
||||
Ok(workdir)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -724,6 +724,23 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
extra_files.extend(crate::dynamic::lang::java_owasp_stubs::owasp_stub_files());
|
||||
}
|
||||
|
||||
// FILE_IO (path-traversal) entry-driven confirmation: plant a canary at the
|
||||
// workdir root whose CONTENT is the collision-resistant marker, plus an
|
||||
// empty `testfiles/` directory so the `../nyx_pt_canary` payload resolves
|
||||
// (`<workdir>/testfiles/../nyx_pt_canary` → `<workdir>/nyx_pt_canary`). The
|
||||
// Utils stub points `testfileDir` at `<workdir>/testfiles/` (via
|
||||
// `System.getProperty("user.dir")`), and the harness CWD is the workdir. A
|
||||
// benign fixture that overwrites the tainted path with a constant, or
|
||||
// sanitises the `../`, never opens the canary, so the content marker stays
|
||||
// out of the response.
|
||||
if spec.expected_cap == crate::labels::Cap::FILE_IO {
|
||||
extra_files.push(("testfiles/.nyxkeep".to_owned(), String::new()));
|
||||
extra_files.push((
|
||||
crate::dynamic::corpus::path_trav::java::CANARY_FILENAME.to_owned(),
|
||||
crate::dynamic::corpus::path_trav::java::CANARY_MARKER.to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(HarnessSource {
|
||||
source,
|
||||
filename: "NyxHarness.java".to_owned(),
|
||||
|
|
@ -3220,6 +3237,85 @@ fn pre_call_setup(spec: &HarnessSpec) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
/// Extract the request-slot names a servlet keys its source read on so the
|
||||
/// firehose request stub can seed cookies under the right name. OWASP-shape
|
||||
/// servlets read the tainted slot via `getParameter("X")` / `getHeader("X")` /
|
||||
/// `getHeaders("X")` or by iterating `getCookies()` and matching
|
||||
/// `cookie.getName().equals("X")` (often via `SeparateClassRequest`). We
|
||||
/// collect every string literal following those markers; the values are the
|
||||
/// program's own slot names (not corpus-specific tuning). Deduplicated,
|
||||
/// capped, and only simple `"..."` literals (no escapes) are taken.
|
||||
fn servlet_slot_names(source: &str) -> Vec<String> {
|
||||
const MARKERS: &[&str] = &[
|
||||
".equals(\"",
|
||||
"getParameter(\"",
|
||||
"getParameterValues(\"",
|
||||
"getHeader(\"",
|
||||
"getHeaders(\"",
|
||||
"getTheParameter(\"",
|
||||
"getTheCookie(\"",
|
||||
"getTheValue(\"",
|
||||
];
|
||||
let mut names: Vec<String> = Vec::new();
|
||||
for marker in MARKERS {
|
||||
let mut rest = source;
|
||||
while let Some(pos) = rest.find(marker) {
|
||||
let after = &rest[pos + marker.len()..];
|
||||
if let Some(end) = after.find('"') {
|
||||
let lit = &after[..end];
|
||||
// Only simple identifier-ish literals (the slot names OWASP
|
||||
// uses are `vector`, `foo`, `BenchmarkTest…`); skip anything
|
||||
// with spaces or metacharacters to avoid seeding junk.
|
||||
if !lit.is_empty()
|
||||
&& lit.len() <= 64
|
||||
&& lit
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-' || b == b'.')
|
||||
&& !names.iter().any(|n| n == lit)
|
||||
{
|
||||
names.push(lit.to_owned());
|
||||
}
|
||||
rest = &after[end + 1..];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
if names.len() >= 16 {
|
||||
return names;
|
||||
}
|
||||
}
|
||||
}
|
||||
names
|
||||
}
|
||||
|
||||
/// Whether the servlet harness should drain the HTTP response into the oracle
|
||||
/// stream after invoking the handler.
|
||||
///
|
||||
/// Suppressed for `HTML_ESCAPE` (reflected XSS): its only oracle is "the
|
||||
/// `<script>…` marker appears in the response", but that marker IS the payload,
|
||||
/// so ANY reflection — including incidental echoes a non-XSS fixture performs
|
||||
/// (`getWriter().write("…'" + new File(param) + "'…")` in a path-traversal
|
||||
/// testcase) — reproduces it. That is exactly the substring-on-payload
|
||||
/// collision the corpus design forbids (cf. the collision-resistant CODE_EXEC
|
||||
/// computed marker), so reflected XSS has no sound runtime oracle here and the
|
||||
/// response must not feed it; the finding stays NotConfirmed rather than
|
||||
/// risk a confirm whose cap cannot be told apart from the file's labelled one.
|
||||
/// CODE_EXEC / FILE_IO / SQL_QUERY markers are collision-resistant (executed
|
||||
/// command output, planted-canary content, DB-side effect), so their response
|
||||
/// drain stays on.
|
||||
fn servlet_drain_response(spec: &HarnessSpec) -> bool {
|
||||
spec.expected_cap != crate::labels::Cap::HTML_ESCAPE
|
||||
}
|
||||
|
||||
/// Render a Java `new String[]{...}` literal from extracted slot names.
|
||||
fn slot_names_java_array(names: &[String]) -> String {
|
||||
let inner = names
|
||||
.iter()
|
||||
.map(|n| format!("{n:?}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!("new String[]{{{inner}}}")
|
||||
}
|
||||
|
||||
/// Emit the per-shape entry-invocation block. Shapes that need
|
||||
/// reflection plumbing rely on helpers from [`shape_helpers`].
|
||||
fn invoke_for_shape(spec: &HarnessSpec, shape: JavaShape, entry_class: &str) -> String {
|
||||
|
|
@ -3230,11 +3326,19 @@ fn invoke_for_shape(spec: &HarnessSpec, shape: JavaShape, entry_class: &str) ->
|
|||
" String[] mainArgs = new String[] {{ payload }};\n {entry_class}.main(mainArgs);"
|
||||
),
|
||||
JavaShape::ServletDoGet => {
|
||||
format!(" invokeServlet({entry_class}.class, \"doGet\", payload, \"GET\");")
|
||||
let slots = slot_names_java_array(&servlet_slot_names(&read_entry_source(&spec.entry_file)));
|
||||
let drain = servlet_drain_response(spec);
|
||||
format!(
|
||||
" invokeServlet({entry_class}.class, \"doGet\", payload, \"GET\", {slots}, {drain});"
|
||||
)
|
||||
}
|
||||
JavaShape::ServletDoPost => {
|
||||
let slots = slot_names_java_array(&servlet_slot_names(&read_entry_source(&spec.entry_file)));
|
||||
let drain = servlet_drain_response(spec);
|
||||
format!(
|
||||
" invokeServlet({entry_class}.class, \"doPost\", payload, \"POST\", {slots}, {drain});"
|
||||
)
|
||||
}
|
||||
JavaShape::ServletDoPost => format!(
|
||||
" invokeServlet({entry_class}.class, \"doPost\", payload, \"POST\");"
|
||||
),
|
||||
JavaShape::SpringController => {
|
||||
format!(
|
||||
" System.out.println(\"NYX_SPRING_TEST=1\");\n invokeSpringController({entry_class}.class, \"{method}\", payload);"
|
||||
|
|
@ -3278,7 +3382,7 @@ fn shape_uses_reflection(shape: JavaShape) -> bool {
|
|||
/// stub-free path used by many fixtures), the helper falls back to
|
||||
/// `invokeReflective`.
|
||||
const SERVLET_HELPER: &str = r#"
|
||||
static void invokeServlet(Class<?> cls, String methodName, String payload, String httpMethod) throws Exception {
|
||||
static void invokeServlet(Class<?> cls, String methodName, String payload, String httpMethod, String[] slotNames, boolean drainResponse) throws Exception {
|
||||
Method match = null;
|
||||
for (Method m : cls.getDeclaredMethods()) {
|
||||
if (!m.getName().equals(methodName)) continue;
|
||||
|
|
@ -3295,19 +3399,79 @@ const SERVLET_HELPER: &str = r#"
|
|||
}
|
||||
Class<?>[] params = match.getParameterTypes();
|
||||
Object[] args = new Object[params.length];
|
||||
Object respStub = null;
|
||||
for (int i = 0; i < params.length; i++) {
|
||||
Class<?> p = params[i];
|
||||
if (p.equals(String.class)) {
|
||||
args[i] = payload;
|
||||
} else if (p.getName().endsWith("HttpServletRequest")) {
|
||||
args[i] = buildRequestStub(p, payload, httpMethod);
|
||||
args[i] = buildRequestStub(p, payload, httpMethod, slotNames);
|
||||
} else if (p.getName().endsWith("HttpServletResponse")) {
|
||||
args[i] = buildResponseStub(p);
|
||||
respStub = buildResponseStub(p);
|
||||
args[i] = respStub;
|
||||
} else {
|
||||
args[i] = null;
|
||||
}
|
||||
}
|
||||
match.invoke(instance, args);
|
||||
// Entry-driven verification: run the REAL servlet handler with the
|
||||
// malicious request, then drain whatever it wrote to the RESPONSE so
|
||||
// the OutputContains oracle can observe a sink-side echo.
|
||||
//
|
||||
// Soundness: the fixture's own `System.out`/`System.err` (debug prints,
|
||||
// exception traces, `printStackTrace`) frequently echo the raw request
|
||||
// value — e.g. OWASP's path-traversal catch does
|
||||
// `System.out.println("Couldn't open ... '" + fileName + "'")` with the
|
||||
// unescaped payload in `fileName`. For caps whose marker is a literal
|
||||
// substring of the payload (XSS `<script>…`), such an incidental echo
|
||||
// would FALSE-CONFIRM. So we silence the fixture's stdout/stderr during
|
||||
// the invoke (capture into a discarded buffer) and emit ONLY the drained
|
||||
// HTTP response + the `__NYX_SINK_HIT__` sentinel to the real stdout.
|
||||
// The response is the genuine sink output: a benign fixture that escapes
|
||||
// (`ESAPI.encoder().encodeForHTML`) writes `<script>` there, an
|
||||
// unsanitised one writes the raw marker. CODE_EXEC/path-traversal
|
||||
// markers still reach the oracle because the fixture writes the child
|
||||
// process output / file content to the response (`printOSCommandResults`
|
||||
// / `getWriter().write(...)`), not (only) to stdout.
|
||||
java.io.PrintStream realOut = System.out;
|
||||
java.io.PrintStream realErr = System.err;
|
||||
java.io.PrintStream sink = new java.io.PrintStream(new java.io.ByteArrayOutputStream());
|
||||
System.setOut(sink);
|
||||
System.setErr(sink);
|
||||
try {
|
||||
match.invoke(instance, args);
|
||||
} catch (Throwable invokeEx) {
|
||||
// Swallow: the verdict is decided by the collision-resistant oracle
|
||||
// on the drained response (or executed-command / canary marker), NOT
|
||||
// by the exception. Letting it reach the harness's outer catch would
|
||||
// print `cause.getMessage()` to stderr — and OWASP fixtures routinely
|
||||
// build exception messages from the raw request value
|
||||
// (`FileNotFoundException: <workdir>/testfiles/<script>…`), which the
|
||||
// OutputContains oracle scans alongside stdout. For a substring
|
||||
// marker (XSS, where marker == payload) that echo is an unsound
|
||||
// false-confirm vector, so the servlet's own exceptions must not
|
||||
// escape this frame.
|
||||
} finally {
|
||||
System.setOut(realOut);
|
||||
System.setErr(realErr);
|
||||
if (drainResponse) {
|
||||
nyxDrainServletResponse(respStub);
|
||||
}
|
||||
realOut.println("__NYX_SINK_HIT__");
|
||||
realOut.flush();
|
||||
}
|
||||
}
|
||||
|
||||
static void nyxDrainServletResponse(Object respStub) {
|
||||
if (respStub == null) return;
|
||||
try {
|
||||
Method getBody = respStub.getClass().getMethod("getBody");
|
||||
Object body = getBody.invoke(respStub);
|
||||
if (body != null) System.out.println(body.toString());
|
||||
} catch (Throwable ignore) {}
|
||||
try {
|
||||
Object redir = respStub.getClass().getMethod("getRedirectedUrl").invoke(respStub);
|
||||
if (redir != null) System.out.println("__NYX_REDIRECT__:" + redir);
|
||||
} catch (Throwable ignore) {}
|
||||
}
|
||||
|
||||
static Object newDefaultInstance(Class<?> cls) throws Exception {
|
||||
|
|
@ -3316,26 +3480,32 @@ const SERVLET_HELPER: &str = r#"
|
|||
return ctor.newInstance();
|
||||
}
|
||||
|
||||
static Object buildRequestStub(Class<?> reqType, String payload, String method) throws Exception {
|
||||
// Best-effort: invoke a no-arg constructor and call any
|
||||
// `setParameter`/`setMethod` setters the stub exposes. When
|
||||
// the type cannot be instantiated, fall back to null and let
|
||||
// the fixture handle the missing parameter.
|
||||
static Object buildRequestStub(Class<?> reqType, String payload, String method, String[] slotNames) throws Exception {
|
||||
// Instantiate the (Nyx-stub) request and seed the payload into every
|
||||
// source accessor via `nyxSeedTaint` (the firehose) so whichever
|
||||
// accessor/name the servlet reads receives the payload. Cookie reads
|
||||
// are name-matched, so the harness also hands the stub the slot names
|
||||
// it extracted from the servlet source. Legacy setters are kept for
|
||||
// back-compat with non-firehose stub variants.
|
||||
try {
|
||||
Constructor<?> ctor = reqType.getDeclaredConstructor();
|
||||
ctor.setAccessible(true);
|
||||
Object stub = ctor.newInstance();
|
||||
try {
|
||||
Method setParam = reqType.getMethod("setParameter", String.class, String.class);
|
||||
setParam.invoke(stub, "payload", payload);
|
||||
reqType.getMethod("nyxSeedTaint", String.class).invoke(stub, payload);
|
||||
} catch (NoSuchMethodException ignore) {}
|
||||
try {
|
||||
Method setMethod = reqType.getMethod("setMethod", String.class);
|
||||
setMethod.invoke(stub, method);
|
||||
reqType.getMethod("nyxSeedCookieNames", String[].class)
|
||||
.invoke(stub, (Object) (slotNames == null ? new String[0] : slotNames));
|
||||
} catch (NoSuchMethodException ignore) {}
|
||||
try {
|
||||
Method setBody = reqType.getMethod("setBody", String.class);
|
||||
setBody.invoke(stub, payload);
|
||||
reqType.getMethod("setParameter", String.class, String.class).invoke(stub, "payload", payload);
|
||||
} catch (NoSuchMethodException ignore) {}
|
||||
try {
|
||||
reqType.getMethod("setMethod", String.class).invoke(stub, method);
|
||||
} catch (NoSuchMethodException ignore) {}
|
||||
try {
|
||||
reqType.getMethod("setBody", String.class).invoke(stub, payload);
|
||||
} catch (NoSuchMethodException ignore) {}
|
||||
return stub;
|
||||
} catch (NoSuchMethodException e) {
|
||||
|
|
|
|||
|
|
@ -96,6 +96,10 @@ pub fn owasp_stub_files() -> Vec<(String, String)> {
|
|||
"org/springframework/web/util/HtmlUtils.java".to_owned(),
|
||||
html_utils_stub(),
|
||||
),
|
||||
(
|
||||
"org/apache/commons/lang/StringEscapeUtils.java".to_owned(),
|
||||
string_escape_utils_stub(),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -110,25 +114,78 @@ pub fn entry_needs_owasp_stubs(source: &str) -> bool {
|
|||
|| source.contains("org.springframework.dao.")
|
||||
|| source.contains("org.springframework.jdbc.")
|
||||
|| source.contains("org.springframework.web.util.")
|
||||
|| source.contains("org.apache.commons.lang.StringEscapeUtils")
|
||||
}
|
||||
|
||||
fn utils_stub() -> String {
|
||||
// FIDELITY (Track L.12): the real `Utils.encodeForHTML` /
|
||||
// `htmlEscape` / `escapeHtml` delegate to a genuine HTML encoder
|
||||
// (`ESAPI.encoder().encodeForHTML` / Spring `HtmlUtils.htmlEscape`).
|
||||
// The verifier drives the real servlet with a live `<script>` payload,
|
||||
// so the stub MUST escape faithfully: a benign fixture that routes the
|
||||
// tainted value through one of these helpers neutralises the payload,
|
||||
// and a no-op stub would echo the marker raw and FALSE-CONFIRM. Only
|
||||
// an unsanitised raw write (`getWriter().print(param)`) reaches the
|
||||
// response with the live marker. `printOSCommandResults` streams the
|
||||
// child process's stdout/stderr to the response writer (and stdout) so
|
||||
// a CODE_EXEC marker echoed by an injected `echo` reaches the
|
||||
// OutputContains oracle.
|
||||
r#"package org.owasp.benchmark.helpers;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
public class Utils {
|
||||
public static String testfileDir = "/tmp/testfiles/";
|
||||
public static String encodeForHTML(String s) { return s == null ? "" : s; }
|
||||
public static String escapeHtml(String s) { return s == null ? "" : s; }
|
||||
public static String htmlEscape(String s) { return s == null ? "" : s; }
|
||||
// Faithful to the real helper (`user.dir + /testfiles/`); resolves to the
|
||||
// harness workdir's `testfiles/` because the Java harness runs with CWD =
|
||||
// workdir. The FILE_IO path-traversal payload `../nyx_pt_canary` escapes
|
||||
// this one level to the workdir-root canary the emitter plants.
|
||||
public static String testfileDir =
|
||||
System.getProperty("user.dir") + java.io.File.separator + "testfiles" + java.io.File.separator;
|
||||
static String nyxEscapeHtml(String s) {
|
||||
if (s == null) return "";
|
||||
StringBuilder o = new StringBuilder(s.length() + 16);
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char ch = s.charAt(i);
|
||||
switch (ch) {
|
||||
case '&': o.append("&"); break;
|
||||
case '<': o.append("<"); break;
|
||||
case '>': o.append(">"); break;
|
||||
case '"': o.append("""); break;
|
||||
case '\'': o.append("'"); break;
|
||||
case '/': o.append("/"); break;
|
||||
default: o.append(ch);
|
||||
}
|
||||
}
|
||||
return o.toString();
|
||||
}
|
||||
public static String encodeForHTML(Object param) {
|
||||
return param == null ? "" : nyxEscapeHtml(String.valueOf(param));
|
||||
}
|
||||
public static String escapeHtml(String s) { return nyxEscapeHtml(s); }
|
||||
public static String htmlEscape(String s) { return nyxEscapeHtml(s); }
|
||||
public static String getFileFromClasspath(String name, ClassLoader cl) { return name; }
|
||||
public static String getInsecureOSCommandString(ClassLoader cl) { return "/bin/sh"; }
|
||||
public static String getOSCommandString(String cmd) { return cmd == null ? "/bin/sh" : cmd; }
|
||||
public static void printOSCommandResults(Process p, Object response) {
|
||||
try {
|
||||
InputStream is = p.getInputStream();
|
||||
if (is != null) { is.close(); }
|
||||
StringBuilder out = new StringBuilder();
|
||||
try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) { out.append(line).append('\n'); }
|
||||
} catch (IOException ignore) {}
|
||||
try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getErrorStream()))) {
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) { out.append(line).append('\n'); }
|
||||
} catch (IOException ignore) {}
|
||||
String text = out.toString();
|
||||
System.out.print(text);
|
||||
if (response != null) {
|
||||
try {
|
||||
Object w = response.getClass().getMethod("getWriter").invoke(response);
|
||||
w.getClass().getMethod("write", String.class).invoke(w, text);
|
||||
} catch (Exception ignore) {}
|
||||
}
|
||||
try { p.waitFor(); } catch (InterruptedException ignore) {}
|
||||
}
|
||||
}
|
||||
"#
|
||||
|
|
@ -194,15 +251,33 @@ public class LDAPManager {
|
|||
}
|
||||
|
||||
fn separate_class_request_stub() -> String {
|
||||
// Real SeparateClassRequest wraps a servlet request and delegates
|
||||
// getTheParameter / getTheValue through to it; the stub keeps the
|
||||
// public surface but discards the request reference.
|
||||
// FIDELITY (Track L.12): the real `SeparateClassRequest` wraps the
|
||||
// servlet request. `getTheParameter` / `getTheCookie` are TAINTED
|
||||
// sources (delegate to the request, which the Nyx stub firehoses with
|
||||
// the payload); `getTheValue` is a SAFE source — the real helper
|
||||
// returns the constant `"bar"` regardless of input, so benign fixtures
|
||||
// that read through `getTheValue` must NOT receive the payload (else
|
||||
// they false-confirm). Delegating to the (firehosed) request keeps the
|
||||
// tainted accessors live while the constant keeps the safe one safe.
|
||||
r#"package org.owasp.benchmark.helpers;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
public class SeparateClassRequest {
|
||||
public SeparateClassRequest(HttpServletRequest request) {}
|
||||
public String getTheParameter(String name) { return null; }
|
||||
public String getTheValue(String name) { return null; }
|
||||
private HttpServletRequest request;
|
||||
public SeparateClassRequest(HttpServletRequest request) { this.request = request; }
|
||||
public String getTheParameter(String p) { return request == null ? null : request.getParameter(p); }
|
||||
public String getTheCookie(String c) {
|
||||
if (request == null) return "";
|
||||
Cookie[] cookies = request.getCookies();
|
||||
if (cookies != null) {
|
||||
for (Cookie cookie : cookies) {
|
||||
if (cookie.getName().equals(c)) { return cookie.getValue(); }
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
// Safe source: the real helper hard-codes this return.
|
||||
public String getTheValue(String p) { return "bar"; }
|
||||
}
|
||||
"#
|
||||
.to_owned()
|
||||
|
|
@ -231,10 +306,33 @@ public interface ThingInterface {
|
|||
}
|
||||
|
||||
fn esapi_stub() -> String {
|
||||
// FIDELITY (Track L.12): `ESAPI.encoder().encodeForHTML` is a REAL
|
||||
// HTML encoder in upstream ESAPI. OWASP benign fixtures (and the
|
||||
// incidental `getWriter().write(ESAPI.encoder().encodeForHTML(fileName))`
|
||||
// echoes in path-traversal / crypto fixtures) rely on it neutralising
|
||||
// metacharacters. A no-op stub would let a firehosed `<script>` marker
|
||||
// through and FALSE-CONFIRM xss on those files, so the stub escapes
|
||||
// `& < > " ' /` exactly like the real encoder's HTML context.
|
||||
r#"package org.owasp.esapi;
|
||||
public class ESAPI {
|
||||
private static final Encoder ENCODER = new Encoder() {
|
||||
@Override public String encodeForHTML(String s) { return s == null ? "" : s; }
|
||||
@Override public String encodeForHTML(String s) {
|
||||
if (s == null) return "";
|
||||
StringBuilder o = new StringBuilder(s.length() + 16);
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char ch = s.charAt(i);
|
||||
switch (ch) {
|
||||
case '&': o.append("&"); break;
|
||||
case '<': o.append("<"); break;
|
||||
case '>': o.append(">"); break;
|
||||
case '"': o.append("""); break;
|
||||
case '\'': o.append("'"); break;
|
||||
case '/': o.append("/"); break;
|
||||
default: o.append(ch);
|
||||
}
|
||||
}
|
||||
return o.toString();
|
||||
}
|
||||
@Override public String encodeForBase64(byte[] b, boolean wrap) {
|
||||
return b == null ? "" : java.util.Base64.getEncoder().encodeToString(b);
|
||||
}
|
||||
|
|
@ -255,6 +353,35 @@ public interface Encoder {
|
|||
.to_owned()
|
||||
}
|
||||
|
||||
fn string_escape_utils_stub() -> String {
|
||||
// FIDELITY (Track L.12): Apache Commons Lang `StringEscapeUtils.escapeHtml`
|
||||
// is a real HTML encoder used as the XSS defence in benign OWASP fixtures
|
||||
// (`org.apache.commons.lang.StringEscapeUtils.escapeHtml(param)`, inline
|
||||
// FQN). Without the stub javac reports the package missing → BuildFailed;
|
||||
// with a faithful escape a benign escape path neutralises the marker.
|
||||
r#"package org.apache.commons.lang;
|
||||
public class StringEscapeUtils {
|
||||
public static String escapeHtml(String s) {
|
||||
if (s == null) return null;
|
||||
StringBuilder o = new StringBuilder(s.length() + 16);
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char ch = s.charAt(i);
|
||||
switch (ch) {
|
||||
case '&': o.append("&"); break;
|
||||
case '<': o.append("<"); break;
|
||||
case '>': o.append(">"); break;
|
||||
case '"': o.append("""); break;
|
||||
default: o.append(ch);
|
||||
}
|
||||
}
|
||||
return o.toString();
|
||||
}
|
||||
public static String unescapeHtml(String s) { return s; }
|
||||
}
|
||||
"#
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn data_access_exception_stub() -> String {
|
||||
r#"package org.springframework.dao;
|
||||
public class DataAccessException extends RuntimeException {
|
||||
|
|
@ -293,9 +420,29 @@ public interface SqlRowSet {
|
|||
}
|
||||
|
||||
fn html_utils_stub() -> String {
|
||||
// FIDELITY (Track L.12): Spring `HtmlUtils.htmlEscape` is a real HTML
|
||||
// encoder; benign OWASP fixtures use it as the XSS defence. Escape
|
||||
// faithfully so a firehosed `<script>` marker cannot survive a benign
|
||||
// escape path and false-confirm.
|
||||
r#"package org.springframework.web.util;
|
||||
public class HtmlUtils {
|
||||
public static String htmlEscape(String s) { return s == null ? "" : s; }
|
||||
public static String htmlEscape(String s) {
|
||||
if (s == null) return "";
|
||||
StringBuilder o = new StringBuilder(s.length() + 16);
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char ch = s.charAt(i);
|
||||
switch (ch) {
|
||||
case '&': o.append("&"); break;
|
||||
case '<': o.append("<"); break;
|
||||
case '>': o.append(">"); break;
|
||||
case '"': o.append("""); break;
|
||||
case '\'': o.append("'"); break;
|
||||
default: o.append(ch);
|
||||
}
|
||||
}
|
||||
return o.toString();
|
||||
}
|
||||
public static String htmlEscape(String s, String enc) { return htmlEscape(s); }
|
||||
public static String htmlUnescape(String s) { return s == null ? "" : s; }
|
||||
}
|
||||
"#
|
||||
|
|
@ -354,6 +501,7 @@ mod tests {
|
|||
"org/springframework/jdbc/core/RowMapper.java",
|
||||
"org/springframework/jdbc/support/rowset/SqlRowSet.java",
|
||||
"org/springframework/web/util/HtmlUtils.java",
|
||||
"org/apache/commons/lang/StringEscapeUtils.java",
|
||||
] {
|
||||
assert!(
|
||||
paths.iter().any(|p| p == required),
|
||||
|
|
@ -451,8 +599,8 @@ mod tests {
|
|||
let files = owasp_stub_files();
|
||||
assert_eq!(
|
||||
files.len(),
|
||||
13,
|
||||
"expected 9 owasp + 4 springframework stubs"
|
||||
14,
|
||||
"expected 9 owasp + 4 springframework + 1 commons-lang stub"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,23 +165,45 @@ import {pkg}.ServletInputStream;
|
|||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
public class HttpServletRequest {{
|
||||
private final Map<String, String> params = new HashMap<>();
|
||||
private String method = "GET";
|
||||
private String body = "";
|
||||
// ── Nyx taint-firehose seeding (Track L.12) ──────────────────────────────
|
||||
// The dynamic verifier drives the real servlet `doPost`/`doGet` with a
|
||||
// malicious request. Real OWASP-shape servlets read the tainted slot
|
||||
// through one of many accessors (`getParameter`, `getHeader`,
|
||||
// `getHeaders`, `getCookies`, `getQueryString`, `getReader`,
|
||||
// `getInputStream`) keyed on a name the harness does not know a priori.
|
||||
// Rather than guess the slot, the stub returns the seeded payload from
|
||||
// EVERY source accessor (the "firehose"): whichever accessor/name the
|
||||
// servlet reads, it receives the payload. This is sound — the servlet's
|
||||
// OWN sanitiser / branch logic still runs on the firehosed value, so a
|
||||
// benign fixture that neutralises the input (constant-overwrite, real
|
||||
// escaper, validator) still produces no marker; only an unsanitised flow
|
||||
// reaches the sink with the live payload. Cookie slots are name-matched
|
||||
// (`cookie.getName().equals("X")`), so the harness seeds the candidate
|
||||
// names it extracted from the servlet source into `nyxCookieNames`.
|
||||
private String nyxTaint = null;
|
||||
private String[] nyxCookieNames = new String[0];
|
||||
public HttpServletRequest() {{}}
|
||||
public void setParameter(String name, String value) {{ params.put(name, value); }}
|
||||
public void setMethod(String m) {{ this.method = m; }}
|
||||
public void setBody(String b) {{ this.body = b == null ? "" : b; }}
|
||||
public void setBody(String b) {{ this.body = b == null ? "" : b; if (b != null && nyxTaint == null) nyxTaint = b; }}
|
||||
public void nyxSeedTaint(String v) {{ this.nyxTaint = v; if (v != null) this.body = v; }}
|
||||
public void nyxSeedCookieNames(String[] names) {{ if (names != null) this.nyxCookieNames = names; }}
|
||||
public String getBody() {{ return body; }}
|
||||
public String getParameter(String name) {{ return params.get(name); }}
|
||||
public String getParameter(String name) {{ String v = params.get(name); return v != null ? v : nyxTaint; }}
|
||||
public String[] getParameterValues(String name) {{
|
||||
String v = params.get(name);
|
||||
if (v == null) v = nyxTaint;
|
||||
return v == null ? null : new String[] {{ v }};
|
||||
}}
|
||||
public Map<String, String[]> getParameterMap() {{
|
||||
|
|
@ -192,18 +214,37 @@ public class HttpServletRequest {{
|
|||
return m;
|
||||
}}
|
||||
public Enumeration<String> getParameterNames() {{ return Collections.enumeration(params.keySet()); }}
|
||||
public String getHeader(String name) {{ return null; }}
|
||||
public Enumeration<String> getHeaders(String name) {{ return Collections.emptyEnumeration(); }}
|
||||
public String getHeader(String name) {{ return nyxTaint; }}
|
||||
public Enumeration<String> getHeaders(String name) {{
|
||||
return nyxTaint == null
|
||||
? Collections.<String>emptyEnumeration()
|
||||
: Collections.enumeration(Collections.singletonList(nyxTaint));
|
||||
}}
|
||||
public Enumeration<String> getHeaderNames() {{ return Collections.emptyEnumeration(); }}
|
||||
public int getIntHeader(String name) {{ return -1; }}
|
||||
public long getDateHeader(String name) {{ return -1L; }}
|
||||
public Cookie[] getCookies() {{ return null; }}
|
||||
public Cookie[] getCookies() {{
|
||||
if (nyxTaint == null) return null;
|
||||
List<Cookie> cs = new ArrayList<>();
|
||||
for (String n : nyxCookieNames) {{ if (n != null) cs.add(new Cookie(n, nyxTaint)); }}
|
||||
if (cs.isEmpty()) cs.add(new Cookie("vector", nyxTaint));
|
||||
return cs.toArray(new Cookie[0]);
|
||||
}}
|
||||
public HttpSession getSession() {{ return new HttpSession(); }}
|
||||
public HttpSession getSession(boolean create) {{ return new HttpSession(); }}
|
||||
public ServletInputStream getInputStream() throws IOException {{ return null; }}
|
||||
public BufferedReader getReader() throws IOException {{ return new BufferedReader(new StringReader(body)); }}
|
||||
public ServletInputStream getInputStream() throws IOException {{
|
||||
final byte[] data = (nyxTaint != null ? nyxTaint : body)
|
||||
.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
return new ServletInputStream() {{
|
||||
private int pos = 0;
|
||||
@Override public int read() throws IOException {{ return pos < data.length ? (data[pos++] & 0xff) : -1; }}
|
||||
}};
|
||||
}}
|
||||
public BufferedReader getReader() throws IOException {{
|
||||
return new BufferedReader(new StringReader(nyxTaint != null ? nyxTaint : body));
|
||||
}}
|
||||
public String getMethod() {{ return method; }}
|
||||
public String getQueryString() {{ return null; }}
|
||||
public String getQueryString() {{ return nyxTaint; }}
|
||||
public StringBuffer getRequestURL() {{ return new StringBuffer(); }}
|
||||
public String getRequestURI() {{ return ""; }}
|
||||
public String getRemoteAddr() {{ return "127.0.0.1"; }}
|
||||
|
|
|
|||
|
|
@ -1700,7 +1700,7 @@ fn cap_for_rule_category(category: &str) -> Option<Cap> {
|
|||
/// `os.system("echo " + shlex.quote(x))` control no longer false-confirms).
|
||||
/// Other set bits are preserved so a multi-cap sink keeps its other
|
||||
/// (already-driveable) capabilities.
|
||||
fn drivable_expected_cap(cap: Cap) -> Cap {
|
||||
pub(crate) fn drivable_expected_cap(cap: Cap) -> Cap {
|
||||
if cap.contains(Cap::SHELL_ESCAPE) {
|
||||
(cap - Cap::SHELL_ESCAPE) | Cap::CODE_EXEC
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ pub const NYX_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|||
/// [`crate::dynamic::corpus::CORPUS_VERSION`]; the compile-time assertion
|
||||
/// below + the [`corpus_version_const_matches_corpus_module`] runtime test
|
||||
/// jointly guard drift.
|
||||
pub const CORPUS_VERSION: &str = "16";
|
||||
pub const CORPUS_VERSION: &str = "17";
|
||||
|
||||
/// Compile-time guard that pins [`CORPUS_VERSION`] (this module) to the
|
||||
/// textual form of [`crate::dynamic::corpus::CORPUS_VERSION`]. Bumping the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue