# Trestle SSTI in Jinja2 include tags allows arbitrary code execution (Incomplete fix of CVE-2026-46439)

Trestle's md_clean_include/mdsection_include tags re-parse included markdown as Jinja2 in a non-sandboxed Environment, so OSCAL prose fields yield RCE on the CI runner.

- Payout: Research
- Severity: high
- Program: compliance-trestle
- Found by: clzoom
- Disclosed: 2026-09-24
- CVE: CVE-2026-57170
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-mr95-65j8-9mxp
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-mr95-65j8-9mxp
- Tags: area:oss domain:oss impact:integrity impact:rce kind:research platform:ghsa sev:high tech:python technique:incomplete-fix technique:source-review vuln:rce vuln:ssti

**Target:** Jinja2 include tags (MDSectionInclude/MDCleanInclude) in trestle author jinja
**Root cause:** The include tags call Parser(self.environment, <included file text>).parse() in a plain non-sandboxed jinja2.Environment, compiling included markdown as a template; the parent fix only removed the recursive re-render loop in render_template.
**Steps:**
1. Ingest OSCAL content where a prose/description field contains `{{ 7*7 }}` (or a gadget chain); ssp_io.py/docs_control_writer.py write it verbatim, neutralizing `{{` only in parameter tables.
2. Run `trestle author ssp-generate` to produce markdown containing the payload.
3. Run `trestle author jinja -i template.md.j2` where the template uses `{% md_clean_include 'generated.md' %}` or `{% mdsection_include 'generated_ssp.md' '# Some Section' %}`.
4. Include tag re-parses the markdown in the non-sandboxed Environment; observe `49` instead of literal `{{ 7*7 }}`, confirming evaluation.
**How found:** Source review of the parent fix: noticed the recursive re-render loop was removed but the include tags still call Parser(...).parse() on included file content, and that the environment is a plain Environment; then traced the data bridge from OSCAL prose writers, noting `{{` neutralization exists only in _parameter_table.
**Exploitation:** Attacker supplies crafted OSCAL (component-definition/SSP/catalog) to an automated trestle pipeline; prose fields flow into generated markdown, which the include tag re-parses and executes with the CI runner's privileges.
**Insight:** When a template-engine fix removes one re-evaluation sink, audit every other place the engine compiles strings (custom tags calling Parser/compile) and check whether the Environment is sandboxed; also check whether delimiter neutralization is applied uniformly across all writer methods, not just one.

## Proof of concept (copied exactly from the original report)

**Benign SSTI marker in OSCAL prose field**

```
place a benign marker such as {{ 7*7 }} in an OSCAL prose / description field; run the generator that writes it to markdown; include that markdown with {% md_clean_include 'generated.md' %}; observe 49 rather than the literal {{ 7*7 }}, confirming evaluation.
```

**Include tag re-parse sink code**

```
local_parser = Parser(self.environment, md_section.content.raw_text)   # ~line 104  <-- RE-PARSE
  top_level_output = local_parser.parse()                                # ~line 105
```

**Non-sandboxed Environment construction**

```
return Environment(loader=FileSystemLoader(template_folder), extensions=extensions(), trim_blocks=True, autoescape=True)
```

