From 67ffeed78082bd07452de0756926c936b3aef412 Mon Sep 17 00:00:00 2001 From: pitboss Date: Wed, 20 May 2026 21:07:23 -0500 Subject: [PATCH] [pitboss/grind] deferred session-0003 (20260520T233019Z-6958) --- src/dynamic/lang/java.rs | 46 +++++++++++++++++------------------ src/dynamic/lang/js_shared.rs | 39 ++++++++++++++++++++--------- src/dynamic/lang/php.rs | 41 ++++++++++++++++++++++--------- src/dynamic/lang/python.rs | 37 +++++++++++++--------------- src/dynamic/lang/ruby.rs | 20 +++++++-------- 5 files changed, 106 insertions(+), 77 deletions(-) diff --git a/src/dynamic/lang/java.rs b/src/dynamic/lang/java.rs index 77d6c81f..968be30f 100644 --- a/src/dynamic/lang/java.rs +++ b/src/dynamic/lang/java.rs @@ -820,38 +820,36 @@ pub fn emit_ssti_harness(_spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); let source = format!( r#"// Nyx dynamic harness — SSTI Thymeleaf (Phase 04 / Track J.2). +// +// Routes `NYX_PAYLOAD` through the real `org.thymeleaf.TemplateEngine` +// dependency. The corpus vuln payload `[[${{7*7}}]]` reaches +// Thymeleaf's SpEL evaluator and renders as `49`; the benign +// control `7*7` has no `[[${{ ... }}]]` markers so the engine echoes +// it verbatim. +// +// Compile + classpath bootstrap is handled by the brief's Maven +// addendum — the synthetic harness this replaces never linked +// Thymeleaf, so the build path needs `pom.xml` plumbing routed +// through `prepare_java` before a host without `org.thymeleaf` +// on the classpath can run the harness. Until that plumbing +// lands the e2e Java SSTI test SKIPs via the runner's BuildFailed +// branch. import java.io.FileWriter; import java.io.IOException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; public class NyxHarness {{ {shim} static String nyxThymeleafRender(String payload) {{ - Pattern p = Pattern.compile("\\[\\[\\$\\{{(.+?)\\}}\\]\\]"); - Matcher m = p.matcher(payload); - StringBuffer out = new StringBuffer(payload.length()); - while (m.find()) {{ - String expr = m.group(1).trim(); - Matcher mul = Pattern.compile("^(\\d+)\\s*\\*\\s*(\\d+)$").matcher(expr); - Matcher add = Pattern.compile("^(\\d+)\\s*\\+\\s*(\\d+)$").matcher(expr); - String repl; - if (mul.matches()) {{ - long a = Long.parseLong(mul.group(1)); - long b = Long.parseLong(mul.group(2)); - repl = Long.toString(a * b); - }} else if (add.matches()) {{ - long a = Long.parseLong(add.group(1)); - long b = Long.parseLong(add.group(2)); - repl = Long.toString(a + b); - }} else {{ - repl = Matcher.quoteReplacement(m.group(0)); - }} - m.appendReplacement(out, Matcher.quoteReplacement(repl)); + try {{ + TemplateEngine engine = new TemplateEngine(); + Context ctx = new Context(); + return engine.process(payload, ctx); + }} catch (RuntimeException e) {{ + return ""; }} - m.appendTail(out); - return out.toString(); }} static void nyxSstiProbe(String rendered) {{ diff --git a/src/dynamic/lang/js_shared.rs b/src/dynamic/lang/js_shared.rs index 914d5c86..f9d6c4a3 100644 --- a/src/dynamic/lang/js_shared.rs +++ b/src/dynamic/lang/js_shared.rs @@ -1074,20 +1074,30 @@ pub fn emit_ssti_harness(_spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); let body = format!( r#"// Nyx dynamic harness — SSTI Handlebars (Phase 04 / Track J.2). +// +// Routes `NYX_PAYLOAD` through the real `handlebars` npm package's +// `compile(payload)({{}})` call. Handlebars does not evaluate +// arithmetic in `{{{{ ... }}}}` blocks by itself; the corpus vuln +// payload `{{{{multiply 7 7}}}}` invokes a registered `multiply` +// helper which returns `49`. The benign control `7*7` has no +// `{{{{` / `}}}}` markers so the engine echoes it verbatim. {shim} +const Handlebars = require('handlebars'); + +Handlebars.registerHelper('multiply', function (a, b) {{ + return String(Number(a) * Number(b)); +}}); +Handlebars.registerHelper('add', function (a, b) {{ + return String(Number(a) + Number(b)); +}}); + function nyxHandlebarsRender(payload) {{ - return payload.replace(/\{{\{{(.+?)\}}\}}/g, function (_, raw) {{ - const expr = raw.trim(); - const helperMatch = expr.match(/^(\w+)\s+(\d+)\s+(\d+)$/); - if (helperMatch) {{ - const a = parseInt(helperMatch[2], 10); - const b = parseInt(helperMatch[3], 10); - if (helperMatch[1] === 'multiply') return String(a * b); - if (helperMatch[1] === 'add') return String(a + b); - }} - return _; - }}); + try {{ + return Handlebars.compile(payload)({{}}); + }} catch (e) {{ + return ''; + }} }} function nyxSstiProbe(rendered) {{ @@ -1119,7 +1129,12 @@ console.log(JSON.stringify({{ render: rendered }})); source: body, filename: "harness.js".to_owned(), command: vec!["node".to_owned(), "harness.js".to_owned()], - extra_files: Vec::new(), + extra_files: vec![( + "package.json".to_owned(), + r#"{"name":"nyx-ssti-handlebars-harness","private":true,"dependencies":{"handlebars":"^4.7.8"}} +"# + .to_owned(), + )], entry_subpath: None, } } diff --git a/src/dynamic/lang/php.rs b/src/dynamic/lang/php.rs index 8fe1a0a6..70f3568a 100644 --- a/src/dynamic/lang/php.rs +++ b/src/dynamic/lang/php.rs @@ -603,19 +603,25 @@ pub fn emit_ssti_harness(_spec: &HarnessSpec) -> HarnessSource { let body = format!( r#"render([])` +// call. The corpus vuln payload `{{{{7*7}}}}` reaches Twig's +// expression evaluator and renders as `49`; the benign control +// `7*7` has no `{{{{` / `}}}}` markers so the engine echoes it +// verbatim. +require_once __DIR__ . '/vendor/autoload.php'; + {shim} function _nyx_twig_render(string $payload): string {{ - return preg_replace_callback('/\{{\{{(.+?)\}}\}}/', function ($m) {{ - $expr = trim($m[1]); - if (preg_match('/^(\d+)\s*\*\s*(\d+)$/', $expr, $mm)) {{ - return (string) ((int) $mm[1] * (int) $mm[2]); - }} - if (preg_match('/^(\d+)\s*\+\s*(\d+)$/', $expr, $mm)) {{ - return (string) ((int) $mm[1] + (int) $mm[2]); - }} - return $m[0]; - }}, $payload) ?? $payload; + try {{ + $twig = new \Twig\Environment(new \Twig\Loader\ArrayLoader([])); + $template = $twig->createTemplate($payload); + return $template->render([]); + }} catch (\Throwable $e) {{ + return ''; + }} }} function _nyx_ssti_probe(string $rendered): void {{ @@ -643,7 +649,20 @@ echo json_encode(["render" => $rendered]) . "\n"; source: body, filename: "harness.php".to_owned(), command: vec!["php".to_owned(), "harness.php".to_owned()], - extra_files: vec![], + extra_files: vec![( + "composer.json".to_owned(), + r#"{ + "name": "nyx/ssti-twig-harness", + "require": { + "twig/twig": "^3.0" + }, + "config": { + "preferred-install": "dist" + } +} +"# + .to_owned(), + )], entry_subpath: None, } } diff --git a/src/dynamic/lang/python.rs b/src/dynamic/lang/python.rs index 812d9abf..56532a53 100644 --- a/src/dynamic/lang/python.rs +++ b/src/dynamic/lang/python.rs @@ -1380,24 +1380,22 @@ pub fn emit_ssti_harness(_spec: &HarnessSpec) -> HarnessSource { let probe = probe_shim(); let body = format!( r#"#!/usr/bin/env python3 -"""Nyx dynamic harness — SSTI Jinja2 (Phase 04 / Track J.2).""" -import os, json, re, sys +"""Nyx dynamic harness — SSTI Jinja2 (Phase 04 / Track J.2). + +Routes `NYX_PAYLOAD` through the real `jinja2.Template(...).render()` +call. The corpus vuln payload `{{{{7*7}}}}` reaches Jinja2's +expression evaluator and renders as `49`; the benign control `7*7` +has no `{{{{` / `}}}}` markers so the engine echoes it verbatim. +""" +import os, json, sys {probe} +import jinja2 + def _nyx_jinja2_render(payload): - # Concretised Jinja2 evaluator for the corpus payloads: substitutes - # arithmetic inside `{{` / `}}` markers and echoes everything else. - def _eval(match): - expr = match.group(1).strip() - m = re.match(r"^(\d+)\s*\*\s*(\d+)$", expr) - if m: - return str(int(m.group(1)) * int(m.group(2))) - m = re.match(r"^(\d+)\s*\+\s*(\d+)$", expr) - if m: - return str(int(m.group(1)) + int(m.group(2))) - return match.group(0) - return re.sub(r"\{{\{{(.+?)\}}\}}", _eval, payload) + template = jinja2.Template(payload) + return template.render() def _nyx_ssti_probe(rendered): rec = {{ @@ -1416,13 +1414,12 @@ def __nyx_now_ns(): def _nyx_run(): payload = os.environ.get("NYX_PAYLOAD", "") - rendered = _nyx_jinja2_render(payload) + try: + rendered = _nyx_jinja2_render(payload) + except jinja2.TemplateError as exc: + rendered = "".format(type(exc).__name__) _nyx_ssti_probe(rendered) - # Sink-hit sentinel — flips SandboxOutcome.sink_hit so the runner's - # `vuln_fired && sink_hit` gate clears. print("__NYX_SINK_HIT__", flush=True) - # Render JSON body — the TemplateEvalEqual predicate compares the - # `render` field's integer value against the corpus `expected`. sys.stdout.write(json.dumps({{"render": rendered}}) + "\n") sys.stdout.flush() @@ -1434,7 +1431,7 @@ if __name__ == "__main__": source: body, filename: "harness.py".to_owned(), command: vec!["python3".to_owned(), "harness.py".to_owned()], - extra_files: Vec::new(), + extra_files: vec![("requirements.txt".to_owned(), "Jinja2\n".to_owned())], entry_subpath: None, } } diff --git a/src/dynamic/lang/ruby.rs b/src/dynamic/lang/ruby.rs index 50def993..09c901a3 100644 --- a/src/dynamic/lang/ruby.rs +++ b/src/dynamic/lang/ruby.rs @@ -921,20 +921,21 @@ pub fn emit_ssti_harness(_spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); let body = format!( r#"# Nyx dynamic harness — SSTI ERB (Phase 04 / Track J.2). +# +# Routes `NYX_PAYLOAD` through the real stdlib `ERB.new(payload).result` +# call. The corpus vuln payload `<%= 7*7 %>` reaches ERB's Ruby +# expression evaluator and renders as `49`; the benign control `7*7` +# has no `<%= ... %>` markers so the engine echoes it verbatim. +require 'erb' require 'json' {shim} def _nyx_erb_render(payload) - payload.gsub(/<%=\s*([^%]+?)\s*%>/) do - expr = Regexp.last_match(1).strip - if (m = expr.match(/\A(\d+)\s*\*\s*(\d+)\z/)) - (m[1].to_i * m[2].to_i).to_s - elsif (m = expr.match(/\A(\d+)\s*\+\s*(\d+)\z/)) - (m[1].to_i + m[2].to_i).to_s - else - Regexp.last_match(0) - end + begin + ERB.new(payload).result(binding) + rescue ScriptError, StandardError => e + "" end end @@ -955,7 +956,6 @@ end payload = ENV['NYX_PAYLOAD'] || '' rendered = _nyx_erb_render(payload) _nyx_ssti_probe(rendered) -# Sink-hit sentinel and render JSON body. STDOUT.puts '__NYX_SINK_HIT__' STDOUT.puts JSON.generate({{"render" => rendered}}) STDOUT.flush