[pitboss/grind] deferred session-0033 (20260517T044708Z-e058)

This commit is contained in:
pitboss 2026-05-17 11:34:08 -05:00
parent b41b24c416
commit fbd5700578
3 changed files with 579 additions and 1 deletions

View file

@ -556,6 +556,17 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
let shape = JavaShape::detect(spec, &entry_source);
let entry_class = derive_entry_class(&entry_source);
let source = generate_harness_java(spec, shape, &entry_class);
let extra_files = match shape {
// Real-world servlet sources import `javax.servlet.*` or
// `jakarta.servlet.*`; without those symbols on the classpath
// `javac` reports `package javax.servlet does not exist` and the
// verifier flips to `BuildFailed`. Stage minimal stubs alongside
// the harness so the build step links.
JavaShape::ServletDoGet | JavaShape::ServletDoPost => {
crate::dynamic::lang::java_servlet_stubs::servlet_stub_files()
}
_ => vec![],
};
Ok(HarnessSource {
source,
@ -566,7 +577,7 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
".".to_owned(),
"NyxHarness".to_owned(),
],
extra_files: vec![],
extra_files,
// Stage the entry file under the public-class-derived filename
// so javac's filename-vs-public-class invariant holds for both
// the legacy `public class Entry` fixtures (which keep being
@ -1108,6 +1119,100 @@ mod tests {
assert!(src.contains("invokeJunitTest(Vuln.class"));
}
// ── Servlet stub bundle (path (a) of Phase 31 budget gate) ──────────────
fn stage_entry(dir: &std::path::Path, name: &str, body: &str) -> String {
let path = dir.join(name);
std::fs::write(&path, body).expect("stage java entry source");
path.to_string_lossy().into_owned()
}
#[test]
fn emit_servlet_doget_carries_servlet_stub_bundle() {
let tmp = tempfile::TempDir::new().unwrap();
let entry_file = stage_entry(
tmp.path(),
"Vuln.java",
"import javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\npublic class Vuln {\n public void doGet(HttpServletRequest r, HttpServletResponse w) {}\n}\n",
);
let mut spec = make_spec_with(EntryKind::HttpRoute, "doGet", &entry_file);
spec.payload_slot = PayloadSlot::QueryParam("payload".into());
let harness = emit(&spec).unwrap();
let paths: Vec<&str> = harness.extra_files.iter().map(|(p, _)| p.as_str()).collect();
assert!(
paths.contains(&"javax/servlet/http/HttpServletRequest.java"),
"doGet bundle missing javax HttpServletRequest stub; got {paths:?}"
);
assert!(
paths.contains(&"jakarta/servlet/http/HttpServletRequest.java"),
"doGet bundle missing jakarta HttpServletRequest stub; got {paths:?}"
);
assert!(paths.contains(&"javax/servlet/annotation/WebServlet.java"));
assert!(paths.contains(&"javax/servlet/ServletException.java"));
}
#[test]
fn emit_servlet_dopost_carries_servlet_stub_bundle() {
let tmp = tempfile::TempDir::new().unwrap();
let entry_file = stage_entry(
tmp.path(),
"Vuln.java",
"import jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\npublic class Vuln {\n public void doPost(HttpServletRequest r, HttpServletResponse w) {}\n}\n",
);
let mut spec = make_spec_with(EntryKind::HttpRoute, "doPost", &entry_file);
spec.payload_slot = PayloadSlot::HttpBody;
let harness = emit(&spec).unwrap();
assert!(!harness.extra_files.is_empty(), "doPost bundle is empty");
let paths: Vec<&str> = harness.extra_files.iter().map(|(p, _)| p.as_str()).collect();
assert!(paths.contains(&"javax/servlet/http/HttpServlet.java"));
assert!(paths.contains(&"jakarta/servlet/http/HttpServlet.java"));
}
#[test]
fn emit_static_method_carries_no_extra_files() {
// Regression guard: non-servlet shapes must not pay the servlet
// stub cost. Adding stubs would balloon the workdir + compile
// time for every Rust / Python / etc. harness too.
let spec = make_spec(PayloadSlot::Param(0));
let harness = emit(&spec).unwrap();
assert!(
harness.extra_files.is_empty(),
"non-servlet shape unexpectedly ships extra files: {:?}",
harness.extra_files.iter().map(|(p, _)| p).collect::<Vec<_>>()
);
}
#[test]
fn emit_static_main_carries_no_extra_files() {
let tmp = tempfile::TempDir::new().unwrap();
let entry_file = stage_entry(
tmp.path(),
"Vuln.java",
"public class Vuln { public static void main(String[] args) {} }\n",
);
let spec = make_spec_with(EntryKind::CliSubcommand, "main", &entry_file);
let harness = emit(&spec).unwrap();
assert!(harness.extra_files.is_empty());
}
#[test]
fn emit_spring_controller_carries_no_servlet_stubs() {
// Spring controllers do not import `javax.servlet.*`; shipping
// the bundle would still compile fine but adds dead `.class`
// files to the workdir. Keep the bundle scoped to actual
// servlet shapes.
let tmp = tempfile::TempDir::new().unwrap();
let entry_file = stage_entry(
tmp.path(),
"Vuln.java",
"@RestController\npublic class Vuln {\n @GetMapping(\"/x\") public String run(String p) { return p; }\n}\n",
);
let mut spec = make_spec_with(EntryKind::HttpRoute, "run", &entry_file);
spec.payload_slot = PayloadSlot::Param(0);
let harness = emit(&spec).unwrap();
assert!(harness.extra_files.is_empty());
}
#[test]
fn entry_class_parses_public_class_declaration() {
assert_eq!(derive_entry_class("public class Vuln {}"), "Vuln");

View file

@ -0,0 +1,472 @@
//! Minimal `javax.servlet` and `jakarta.servlet` stubs for Java harnesses.
//!
//! Many real-world Java codebases (OWASP Benchmark, Spring servlet adapters,
//! legacy webapps) carry `import javax.servlet.http.HttpServletRequest;` or
//! the `jakarta.servlet` counterpart in source files the dynamic verifier
//! wants to compile. Without these symbols on the classpath, `javac` emits
//! `error: package javax.servlet does not exist` and the verifier reports
//! `BuildFailed` before any sink probe runs.
//!
//! The stubs here cover the surface the harness actually needs to link:
//! a no-arg-constructible `HttpServletRequest` whose `setParameter` /
//! `setMethod` / `setBody` setters slot into the reflective adapter in
//! `SERVLET_HELPER`, plus the small set of getters that OWASP Benchmark
//! fixtures call (`getCookies`, `getHeader`, `getInputStream`, `getReader`,
//! `getParameter`, `getParameterMap`, `getParameterNames`,
//! `getParameterValues`, `getQueryString`, `getSession`). Methods return
//! null / empty defaults; runtime correctness past compilation is the job
//! of the spec-derivation fallback paths, not the build-time stubs.
//!
//! The bundle ships both `javax.servlet` and `jakarta.servlet` so source
//! files predating the EE 9 rename and source files using the new
//! namespace both link. Each stub is generated from the same template via
//! [`make_servlet_stubs`] so the two trees stay in sync.
/// Stub bundle for the servlet-shape Java harnesses.
///
/// Returns `(workdir_relative_path, file_content)` pairs ready to drop into
/// [`crate::dynamic::lang::HarnessSource::extra_files`]. Subdirectories in
/// the path are created by the harness builder; the bundled files live
/// under `javax/servlet/...` and `jakarta/servlet/...` so `javac -d <out>`
/// drops the resulting `.class` files into matching package directories
/// and the entry source's `import javax.servlet.http.HttpServletRequest;`
/// resolves at compile time.
pub fn servlet_stub_files() -> Vec<(String, String)> {
let mut out = make_servlet_stubs("javax.servlet");
out.extend(make_servlet_stubs("jakarta.servlet"));
out
}
/// Render the nine-file stub set under the given package prefix
/// (`javax.servlet` or `jakarta.servlet`).
fn make_servlet_stubs(pkg: &str) -> Vec<(String, String)> {
let pkg_path = pkg.replace('.', "/");
let http = format!("{pkg}.http");
let http_path = format!("{pkg_path}/http");
vec![
(
format!("{pkg_path}/ServletException.java"),
servlet_exception(pkg),
),
(
format!("{pkg_path}/ServletInputStream.java"),
servlet_input_stream(pkg),
),
(
format!("{pkg_path}/RequestDispatcher.java"),
request_dispatcher(pkg, &http),
),
(
format!("{pkg_path}/annotation/WebServlet.java"),
web_servlet(pkg),
),
(format!("{http_path}/HttpServlet.java"), http_servlet(pkg)),
(
format!("{http_path}/HttpServletRequest.java"),
http_servlet_request(pkg, &http),
),
(
format!("{http_path}/HttpServletResponse.java"),
http_servlet_response(&http),
),
(
format!("{http_path}/HttpSession.java"),
http_session(&http),
),
(format!("{http_path}/Cookie.java"), cookie(&http)),
]
}
fn servlet_exception(pkg: &str) -> String {
format!(
r#"package {pkg};
public class ServletException extends Exception {{
public ServletException() {{ super(); }}
public ServletException(String msg) {{ super(msg); }}
public ServletException(String msg, Throwable cause) {{ super(msg, cause); }}
public ServletException(Throwable cause) {{ super(cause); }}
}}
"#
)
}
fn servlet_input_stream(pkg: &str) -> String {
format!(
r#"package {pkg};
import java.io.InputStream;
import java.io.IOException;
public abstract class ServletInputStream extends InputStream {{
protected ServletInputStream() {{}}
@Override public int read() throws IOException {{ return -1; }}
}}
"#
)
}
fn request_dispatcher(pkg: &str, http: &str) -> String {
format!(
r#"package {pkg};
import {http}.HttpServletRequest;
import {http}.HttpServletResponse;
import java.io.IOException;
public interface RequestDispatcher {{
void forward(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException;
void include(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException;
}}
"#
)
}
fn web_servlet(pkg: &str) -> String {
format!(
r#"package {pkg}.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WebServlet {{
String[] value() default {{}};
String[] urlPatterns() default {{}};
String name() default "";
int loadOnStartup() default -1;
boolean asyncSupported() default false;
}}
"#
)
}
fn http_servlet(pkg: &str) -> String {
format!(
r#"package {pkg}.http;
import {pkg}.ServletException;
import java.io.IOException;
public abstract class HttpServlet {{
public HttpServlet() {{}}
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {{}}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {{}}
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {{}}
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {{}}
protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {{}}
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {{}}
protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {{}}
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {{}}
public String getServletInfo() {{ return ""; }}
public void init() throws ServletException {{}}
public void destroy() {{}}
}}
"#
)
}
fn http_servlet_request(pkg: &str, http: &str) -> String {
format!(
r#"package {http};
import {pkg}.RequestDispatcher;
import {pkg}.ServletInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
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 = "";
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 String getBody() {{ return body; }}
public String getParameter(String name) {{ return params.get(name); }}
public String[] getParameterValues(String name) {{
String v = params.get(name);
return v == null ? null : new String[] {{ v }};
}}
public Map<String, String[]> getParameterMap() {{
Map<String, String[]> m = new HashMap<>();
for (Map.Entry<String, String> e : params.entrySet()) {{
m.put(e.getKey(), new String[] {{ e.getValue() }});
}}
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 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 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 String getMethod() {{ return method; }}
public String getQueryString() {{ return null; }}
public StringBuffer getRequestURL() {{ return new StringBuffer(); }}
public String getRequestURI() {{ return ""; }}
public String getRemoteAddr() {{ return "127.0.0.1"; }}
public String getRemoteHost() {{ return "localhost"; }}
public String getServletPath() {{ return ""; }}
public String getContextPath() {{ return ""; }}
public String getPathInfo() {{ return null; }}
public String getPathTranslated() {{ return null; }}
public Object getAttribute(String name) {{ return null; }}
public void setAttribute(String name, Object value) {{}}
public void removeAttribute(String name) {{}}
public Enumeration<String> getAttributeNames() {{ return Collections.emptyEnumeration(); }}
public String getCharacterEncoding() {{ return "UTF-8"; }}
public void setCharacterEncoding(String enc) {{}}
public String getContentType() {{ return null; }}
public int getContentLength() {{ return body == null ? 0 : body.length(); }}
public long getContentLengthLong() {{ return getContentLength(); }}
public String getProtocol() {{ return "HTTP/1.1"; }}
public String getScheme() {{ return "http"; }}
public String getServerName() {{ return "localhost"; }}
public int getServerPort() {{ return 80; }}
public Locale getLocale() {{ return Locale.getDefault(); }}
public Enumeration<Locale> getLocales() {{ return Collections.enumeration(Collections.singletonList(Locale.getDefault())); }}
public RequestDispatcher getRequestDispatcher(String path) {{ return null; }}
public boolean isSecure() {{ return false; }}
public String getAuthType() {{ return null; }}
public String getRemoteUser() {{ return null; }}
public java.security.Principal getUserPrincipal() {{ return null; }}
public boolean isUserInRole(String role) {{ return false; }}
public String getRequestedSessionId() {{ return null; }}
public boolean isRequestedSessionIdValid() {{ return false; }}
public boolean isRequestedSessionIdFromCookie() {{ return false; }}
public boolean isRequestedSessionIdFromURL() {{ return false; }}
}}
"#
)
}
fn http_servlet_response(http: &str) -> String {
format!(
r#"package {http};
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
public class HttpServletResponse {{
public static final int SC_OK = 200;
public static final int SC_NOT_FOUND = 404;
public static final int SC_FORBIDDEN = 403;
public static final int SC_UNAUTHORIZED = 401;
public static final int SC_INTERNAL_SERVER_ERROR = 500;
public static final int SC_MOVED_PERMANENTLY = 301;
public static final int SC_MOVED_TEMPORARILY = 302;
private final StringWriter sw = new StringWriter();
private final PrintWriter pw = new PrintWriter(sw);
private int status = SC_OK;
public HttpServletResponse() {{}}
public PrintWriter getWriter() throws IOException {{ return pw; }}
public String getBody() {{ pw.flush(); return sw.toString(); }}
public OutputStream getOutputStream() throws IOException {{ return new java.io.ByteArrayOutputStream(); }}
public void setContentType(String type) {{}}
public String getContentType() {{ return null; }}
public void setCharacterEncoding(String enc) {{}}
public String getCharacterEncoding() {{ return "UTF-8"; }}
public void setContentLength(int len) {{}}
public void setContentLengthLong(long len) {{}}
public void setStatus(int sc) {{ this.status = sc; }}
public int getStatus() {{ return status; }}
public void sendError(int sc) throws IOException {{ this.status = sc; }}
public void sendError(int sc, String msg) throws IOException {{ this.status = sc; }}
public void sendRedirect(String location) throws IOException {{ this.status = SC_MOVED_TEMPORARILY; }}
public void addCookie(Cookie cookie) {{}}
public void setHeader(String name, String value) {{}}
public void addHeader(String name, String value) {{}}
public void setIntHeader(String name, int value) {{}}
public void addIntHeader(String name, int value) {{}}
public void setDateHeader(String name, long date) {{}}
public void addDateHeader(String name, long date) {{}}
public boolean containsHeader(String name) {{ return false; }}
public String getHeader(String name) {{ return null; }}
public java.util.Collection<String> getHeaders(String name) {{ return java.util.Collections.emptyList(); }}
public java.util.Collection<String> getHeaderNames() {{ return java.util.Collections.emptyList(); }}
public String encodeURL(String url) {{ return url; }}
public String encodeRedirectURL(String url) {{ return url; }}
public void flushBuffer() throws IOException {{}}
public void resetBuffer() {{}}
public void reset() {{}}
public boolean isCommitted() {{ return false; }}
public void setBufferSize(int size) {{}}
public int getBufferSize() {{ return 0; }}
public void setLocale(java.util.Locale loc) {{}}
public java.util.Locale getLocale() {{ return java.util.Locale.getDefault(); }}
}}
"#
)
}
fn http_session(http: &str) -> String {
format!(
r#"package {http};
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
public class HttpSession {{
private final Map<String, Object> attrs = new HashMap<>();
public HttpSession() {{}}
public String getId() {{ return "stub-session"; }}
public void setAttribute(String name, Object value) {{ attrs.put(name, value); }}
public Object getAttribute(String name) {{ return attrs.get(name); }}
public void removeAttribute(String name) {{ attrs.remove(name); }}
public Enumeration<String> getAttributeNames() {{ return Collections.enumeration(attrs.keySet()); }}
public long getCreationTime() {{ return 0L; }}
public long getLastAccessedTime() {{ return 0L; }}
public int getMaxInactiveInterval() {{ return 0; }}
public void setMaxInactiveInterval(int interval) {{}}
public boolean isNew() {{ return true; }}
public void invalidate() {{}}
public void putValue(String name, Object value) {{ attrs.put(name, value); }}
public Object getValue(String name) {{ return attrs.get(name); }}
public String[] getValueNames() {{ return attrs.keySet().toArray(new String[0]); }}
public void removeValue(String name) {{ attrs.remove(name); }}
}}
"#
)
}
fn cookie(http: &str) -> String {
format!(
r#"package {http};
public class Cookie implements Cloneable {{
private String name;
private String value;
private String path;
private String domain;
private String comment;
private int maxAge = -1;
private int version = 0;
private boolean secure;
private boolean httpOnly;
public Cookie() {{}}
public Cookie(String name, String value) {{ this.name = name; this.value = value; }}
public String getName() {{ return name; }}
public String getValue() {{ return value; }}
public void setValue(String value) {{ this.value = value; }}
public void setPath(String path) {{ this.path = path; }}
public String getPath() {{ return path; }}
public void setDomain(String domain) {{ this.domain = domain; }}
public String getDomain() {{ return domain; }}
public void setMaxAge(int age) {{ this.maxAge = age; }}
public int getMaxAge() {{ return maxAge; }}
public void setSecure(boolean secure) {{ this.secure = secure; }}
public boolean getSecure() {{ return secure; }}
public void setHttpOnly(boolean httpOnly) {{ this.httpOnly = httpOnly; }}
public boolean isHttpOnly() {{ return httpOnly; }}
public void setVersion(int v) {{ this.version = v; }}
public int getVersion() {{ return version; }}
public void setComment(String c) {{ this.comment = c; }}
public String getComment() {{ return comment; }}
@Override public Object clone() {{ try {{ return super.clone(); }} catch (CloneNotSupportedException e) {{ throw new RuntimeException(e); }} }}
}}
"#
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ships_both_javax_and_jakarta_trees() {
let files = servlet_stub_files();
let paths: Vec<&str> = files.iter().map(|(p, _)| p.as_str()).collect();
assert!(paths.contains(&"javax/servlet/http/HttpServletRequest.java"));
assert!(paths.contains(&"javax/servlet/ServletException.java"));
assert!(paths.contains(&"javax/servlet/annotation/WebServlet.java"));
assert!(paths.contains(&"jakarta/servlet/http/HttpServletRequest.java"));
assert!(paths.contains(&"jakarta/servlet/ServletException.java"));
assert!(paths.contains(&"jakarta/servlet/annotation/WebServlet.java"));
}
#[test]
fn bundle_has_eighteen_files() {
// Nine stubs per package tree, two trees. A drift here usually
// means a stub was added without updating the count assertion or
// a stub got accidentally dropped.
let files = servlet_stub_files();
assert_eq!(files.len(), 18, "expected 9 javax + 9 jakarta stubs");
}
#[test]
fn http_servlet_request_carries_owasp_method_surface() {
// OWASP Benchmark v1.2 fixtures exercise this surface; if any of
// these getters disappears the bundle will start producing
// BuildFailed verdicts on the corresponding fixtures.
let req = http_servlet_request("javax.servlet", "javax.servlet.http");
for method in &[
"getCookies",
"getHeader",
"getHeaderNames",
"getHeaders",
"getInputStream",
"getParameter",
"getParameterMap",
"getParameterNames",
"getParameterValues",
"getQueryString",
"getSession",
"getReader",
] {
assert!(
req.contains(method),
"javax HttpServletRequest stub missing `{method}`"
);
}
}
#[test]
fn http_servlet_response_carries_owasp_method_surface() {
let resp = http_servlet_response("javax.servlet.http");
for method in &["addCookie", "getWriter", "setContentType"] {
assert!(
resp.contains(method),
"javax HttpServletResponse stub missing `{method}`"
);
}
}
#[test]
fn http_servlet_request_keeps_reflective_hook_setters() {
// The Java emitter's SERVLET_HELPER uses reflection to invoke
// setParameter / setMethod / setBody on the stub request before
// the entry method runs. Dropping any of these would silently
// break payload seeding for OWASP-shape harnesses.
for pkg in &["javax.servlet", "jakarta.servlet"] {
let http = format!("{pkg}.http");
let req = http_servlet_request(pkg, &http);
for method in &["setParameter", "setMethod", "setBody"] {
assert!(
req.contains(method),
"{pkg} HttpServletRequest stub missing `{method}`"
);
}
}
}
#[test]
fn jakarta_stubs_carry_jakarta_package_declaration() {
let files = servlet_stub_files();
for (path, body) in &files {
if path.starts_with("jakarta/") {
assert!(
body.contains("package jakarta.servlet"),
"jakarta stub at {path} missing jakarta package declaration"
);
assert!(
!body.contains("package javax.servlet"),
"jakarta stub at {path} accidentally carries javax package"
);
}
}
}
}

View file

@ -16,6 +16,7 @@ pub mod c;
pub mod cpp;
pub mod go;
pub mod java;
pub mod java_servlet_stubs;
pub mod javascript;
pub mod js_shared;
pub mod php;