CVE-2026-57170: Trestle SSTI in Jinja2 include tags allows arbitrary code execution (Incomplete fix of CVE-2026-46439)
Compliance-trestle (Trestle) is a Python SDK and command-line tool for managing OSCAL compliance documents. In versions prior to 3.12.4 and 4.0.0 through 4.0.3, the custom Jinja2 include tags mdsectioninclude and mdcleaninclude re-parse the content of an included Markdown file as Jinja2 template code in a non-sandboxed environment, allowing server-side template injection that can lead to arbitrary code execution. The MDSectionInclude and MDCleanInclude tags in Trestle/core/jinja/tags.py pass included file content to Parser(self.environment, ...).parse(), splicing it into the host template's compilation, and the environment is a plain jinja2.Environment rather than a SandboxedEnvironment, so any expressions in the file are evaluated with full access to the usual SSTI gadget chain. Because Trestle's Markdown writers emit OSCAL prose and component-description fields verbatim, applying delimiter neutralization only to parameter tables, attacker-controlled OSCAL data such as a control statement, part prose, or component description containing Jinja2 syntax flows into an included Markdown file and is executed when the include tag re-parses it. This issue is fixed in version 4.1.0.
Other sources
Reporter: Cavan Loughran, Celvex Group Inc.
Summary ------- The fix for CVE-2026-46439 (3.12.2 / 4.0.3) removed the recursive re-render loop in trestle/core/commands/author/jinja.py rendertemplate, but the custom include tags in trestle/core/jinja/tags.py (MDSectionInclude, MDCleanInclude) still re-parse the CONTENT of an included markdown file as a Jinja2 template via Parser(self.environment, <file text>).parse() in a plain (non-sandboxed) jinja2.Environment. Because the trestle markdown writers (sspio.py SSPMarkdownWriter, docscontrolwriter.py DocsControlWriter) write OSCAL prose / component-description fields verbatim (the {{ -> [[ neutralization is applied to parameter tables only), attacker-controlled OSCAL data that flows into an included markdown file is interpreted as template code and can achieve arbitrary code execution on the runner. This is the same trust boundary and impact as CVE-2026-46439 via a sink the fix did not cover.
Affected versions ----------------- 4.0.3 and earlier on this code path (the include tags predate and were untouched by the parent fix); the 3.12.x line likewise. Verified present in the fixed release 4.0.3 by direct source read.
Technical detail ---------------- Sink (UNCHANGED by the parent fix), trestle/core/jinja/tags.py:
MDSectionInclude.parse (the {% mdsectioninclude 'file.md' '# Section' %} tag), approximately: mdcontent, , = self.environment.loader.getsource(self.environment, markdownsource.value) # ~line 90 ... localparser = Parser(self.environment, mdsection.content.rawtext) # ~line 104 <-- RE-PARSE topleveloutput = localparser.parse() # ~line 105 return topleveloutput.body # ~line 106
MDCleanInclude.parse (the {% mdcleaninclude 'file.md' %} tag), approximately: mdcontent, , = self.environment.loader.getsource(self.environment, markdownsource.value) # ~line 141 ... localparser = Parser(self.environment, content) # ~line 150 <-- RE-PARSE topleveloutput = localparser.parse() # ~line 151 return topleveloutput.body # ~line 152
Parser(self.environment, <included file text>).parse() compiles the included file's markdown text AS A JINJA TEMPLATE and splices the resulting AST .body directly into the host template's compilation. Any {{ ... }} / {% ... %} present in the included file is interpreted as template code, not emitted as literal text. The single-pass fix in rendertemplate has no effect on this: the re-parse happens at compile time inside the tag, on a string the outer render loop never sees as data.
Environment is NOT sandboxed (UNCHANGED by the fix), trestle/core/commands/author/jinja.py createjinjaenvironment: return Environment(loader=FileSystemLoader(templatefolder), extensions=extensions(), trimblocks=True, autoescape=True) This is a plain jinja2.Environment, not jinja2.sandbox.SandboxedEnvironment, so a re-parsed expression has full access to the usual Jinja SSTI gadget chain leading to OS command execution. autoescape=True HTML-escapes output but does not constrain expression evaluation, so it does not mitigate code execution.
Data bridge (partial neutralization only): the included markdown is, in trestle's documented workflow, produced from OSCAL by the trestle generators / writers. trestle/core/sspio.py SSPMarkdownWriter writes control statement prose (getcontrolstatementssp) and component implementation descriptions (prose = bycomp.description) into markdown with no Jinja-delimiter escaping. trestle/core/docscontrolwriter.py DocsControlWriter writes control section prose (self.mdfile.newline(prose)) and part text raw. The maintainers DO know about the {{-in-data risk: sspio.py applies line.replace('{{', '[[').replace('}}', ']]') to neutralize delimiters, but ONLY inside the parameter-table method (parametertable). Control-statement prose and component description fields receive no such neutralization. So an OSCAL field such as an SSP component description, a control statement override, or a catalog part prose string that contains {{ <gadget> }} lands verbatim in the generated markdown, and then the include tag re-parses and executes it.
Why this is a genuine incomplete fix (not the same bug): the parent CVE-2026-46439 re-evaluation site was the recursive loop in rendertemplate (main template output) in commands/author/jinja.py, which the fix removed. THIS variant's re-evaluation site is Parser(self.environment, ...) inside mdsectioninclude / mdcleaninclude (included file content) in core/jinja/tags.py, which the fix did NOT touch, and which runs in a plain (non-sandboxed) Environment. Same class (SSTI -> RCE), same trust boundary (untrusted OSCAL data), different and still-live sink.
Reachability (non-destructive reasoning) ---------------------------------------- 1. A pipeline ingests OSCAL content (component-definition, SSP, profile, or catalog) that an external or lower-trust party can influence (a component supplier, a control-narrative contributor, an upstream catalog). This is precisely the parent's accepted attacker: a malicious actor supplies crafted OSCAL content in an automated pipeline (CI/CD). 2. The pipeline runs trestle author ssp-generate (or equivalent) which writes the OSCAL prose/description fields into markdown verbatim. A field value such as {{ cycler.init.globals['os'].popen('id').read() }} survives intact (no neutralization on prose/description). 3. The pipeline then runs trestle author jinja -i template.md.j2 ... where the template composes the document with {% mdcleaninclude 'generatedcontrol.md' %} or {% mdsectioninclude 'generatedssp.md' '# Some Section' %} (the documented populate-content-from-other-documents pattern). 4. The include tag re-parses the generated markdown in the non-sandboxed environment and the embedded expression executes => code runs with the privileges of the trestle process (the CI runner).
Non-destructive confirmation: place a benign marker such as {{ 77 }} in an OSCAL prose / description field; run the generator that writes it to markdown; include that markdown with {% mdcleaninclude 'generated.md' %}; observe 49 rather than the literal {{ 77 }}, confirming evaluation. A malicious field would use the standard Jinja gadget chain; that is deliberately NOT included here.
Impact ------ Arbitrary code execution on the trestle process (the CI runner), under the same trust boundary that CVE-2026-46439 was assigned on (untrusted OSCAL data in an automated pipeline). Non-sandboxed environment => full RCE in a release that is patched for the parent CVE.
Remediation ----------- (a) render the include re-parse through jinja2.sandbox.SandboxedEnvironment, OR (b) emit the included file content as literal text (do not re-parse it as a template), OR (c) extend the {{ / }} -> [[ / ]] neutralization in sspio.py / docscontrolwriter.py to ALL OSCAL prose / description / part-text writes, not just parameter tables. Option (a) or (b) is the durable fix; (c) is defense-in-depth.
— GitHub
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/compliance-trestleto a version that resolves this vulnerability.Fixed in 4.1.0 - Upgrade
Upgrade
pip/compliance-trestleto a version that resolves this vulnerability.Fixed in 3.12.4 - Upgrade
Upgrade
compliance-trestleto a version that resolves this vulnerability.Fixed in 4.1.0 - Configuration
Change the include implementation so included Markdown is either parsed through jinja2.sandbox.SandboxedEnvironment or emitted as literal text without being re-parsed as Jinja2 template code.
Trestle Jinja include tags (mdsection_include and md_clean_include) included Markdown handling = SandboxedEnvironment parsing or literal text without re-parsing - Configuration
Extend the existing {{ / }} to [[ / ]] neutralization from parameter tables to all OSCAL prose, description, and part-text writes.
Trestle ssp_io.py and docs_control_writer.py Jinja delimiter neutralization for OSCAL prose, description, and part-text writes = replace {{ with [[ and }} with ]]
Event History
Frequently Asked Questions
Which deployments are exposed in practice?
Deployments are exposed when Trestle renders Markdown through the mdsection_include or md_clean_include tags and the included Markdown can contain attacker-controlled OSCAL prose, such as control statements, part prose, or component descriptions. The attacker needs the ability to introduce Jinja2 syntax into that data; the CVSS vector indicates local access with low privileges.
How can I determine whether my installation is affected?
Check the installed Trestle version and whether workflows use the mdsection_include or md_clean_include custom Jinja2 tags. Versions before 3.12.4 and versions from 4.0.0 through 4.0.3 are affected.
What should be done if immediate upgrading is not possible?
Avoid rendering untrusted or externally supplied OSCAL prose through Markdown files used by the vulnerable include tags. In particular, prevent Jinja2 expressions in control statements, part prose, and component descriptions from reaching included Markdown that is re-parsed as a template.
What version resolves the issue?
The issue is fixed in Trestle version 4.1.0.