Skip to content

FIX: Detect multi-engine RCE gadgets in SSTIOutputScorer - #3541

Open
Vardhman Gupta (Kaap10) wants to merge 1 commit into
microsoft:mainfrom
Kaap10:feat/ssti-multi-engine-rce-gadgets
Open

Vardhman Gupta (Kaap10) wants to merge 1 commit into
microsoft:mainfrom
Kaap10:feat/ssti-multi-engine-rce-gadgets

Conversation

@Kaap10

Copy link
Copy Markdown
Contributor

Description

SSTIOutputScorer detects server-side template injection (SSTI) payloads emitted in LLM responses (OWASP LLM02).
While the docstring explicitly states:

"a model can be coaxed into emitting a template expression that a downstream rendering engine (Jinja2, Twig, Freemarker, ERB, Velocity) will evaluate, leading to data disclosure or remote code execution."

The default patterns previously only covered basic arithmetic (\d+ * \d+) and Python-specific sandbox escapes (__class__), completely missing remote code execution gadgets for non-Python template engines commonly used across enterprise systems (Java, PHP):

  1. Apache FreeMarker: Utility class instantiation (freemarker.template.utility.Execute, ObjectConstructor, JythonRuntime) via ?new().
  2. Apache Velocity: Reflection and runtime execution chains (#set($rt=...forName("java.lang.Runtime")), $class.inspect, .getRuntime().exec(), .ProcessBuilder).
  3. Twig (PHP): Dangerous PHP execution filters (|filter('system'), |map('system'), |reduce('system')) and _self.env.registerUndefinedFilterCallback.
  4. Spring Expression Language (SpEL) / Thymeleaf: Java type evaluation into java.lang.Runtime, java.lang.ProcessBuilder, or javax.script (${T(java.lang.Runtime)...}, ${T(Runtime)...}).

This PR extends _DEFAULT_PATTERNS in SSTIOutputScorer with high-precision patterns targeting these canonical exploitation gadgets, while keeping existing patterns and custom overrides completely untouched.

Fixes #3540

Tests and Documentation

  • Added 21 positive test cases in tests/unit/score/regex/test_ssti_output_scorer.py covering FreeMarker, Apache Velocity, Twig (including multiline and whitespace variations), and SpEL (both bare T(Runtime) and fully qualified forms).
  • Added 9 negative control cases verifying that standard template variables, benign filters ({{ names|filter(v => v > 0) }}), benign type references (${T(Math).min(1, 2)}), and shell variables ($100, $user) are not flagged.
  • Added rationale assertions verifying each new pattern name is included in score.score_rationale.
  • All 69 unit tests in test_ssti_output_scorer.py passing cleanly in ~11s.
  • All 1,138 tests across the entire tests/unit/score/regex/ test suite passing with zero regressions.
  • Code style and formatting verified with ruff check and ruff format.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Limit the Velocity scan to its template statement. In pyrit/score/true_false/regex/ssti_output_scorer.py, the new #set alternative uses [\s\S]*? with no statement boundary. A harmless response such as #set($count = 10) followed by a new line containing Documentation: ProcessBuilder is therefore classified as a Velocity gadget: the scan passes the closing parenthesis and takes ProcessBuilder from ordinary text. Please constrain the match to the relevant expression and require an actual gadget use rather than a bare class-name substring. Add a negative case with a harmless #set followed by unrelated prose, alongside the genuine gadget cases.

Review based on code inspection; tests were not run. AI-assisted review (GitHub Copilot).

Detect canonical remote code execution gadgets for Apache FreeMarker, Apache Velocity, Twig, and Spring/SpEL in SSTIOutputScorer.

Fixes microsoft#3540
@Kaap10
Vardhman Gupta (Kaap10) force-pushed the feat/ssti-multi-engine-rce-gadgets branch from c98f4f6 to 9d4b22d Compare October 28, 2026 07:44
@Kaap10

Copy link
Copy Markdown
Contributor Author

Richard Lundeen (@richlundeen) Thanks for the review!

Updated:

  • Bound #set strictly to its assignment expression and removed unconstrained [\s\S]*? to stop cross-statement scanning.
  • Required an actual gadget invocation ((getClass()|class).forName(...), $class.inspect(...), or getRuntime().exec(...)) instead of a bare ProcessBuilder substring.
  • Added the negative case #set($count = 10)\nDocumentation: ProcessBuilder (and other control cases) alongside genuine gadget tests; all 79 unit tests pass cleanly.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: SSTIOutputScorer misses RCE exploitation gadgets for FreeMarker, Velocity, Twig, and Spring/SpEL

2 participants