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

Published Aug 25, 2026
·
Updated

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

6 affected componentsFixes available
trestle Compliance-trestle (Trestle)>3.12.4<=4.0.3
trestle Compliance-trestle (Trestle)>=4.0.0<=4.0.3
trestle Compliance-trestle (Trestle)<4.1.0
pip/compliance-trestle>=4.0.0<4.1.0
4.1.0
pip/compliance-trestle<3.12.4
3.12.4
IBM Concert Software<=1.0-3.0.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/compliance-trestle to a version that resolves this vulnerability.

    Fixed in 4.1.0
  2. Upgrade

    Upgrade pip/compliance-trestle to a version that resolves this vulnerability.

    Fixed in 3.12.4
  3. Upgrade

    Upgrade compliance-trestle to a version that resolves this vulnerability.

    Fixed in 4.1.0
  4. 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
  5. 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

Aug 25, 2026
CVE Published
via MITRE·11:18 PM
Data Sourced
via MITRE·11:18 PM
DescriptionSeverityWeakness
Aug 26, 2026
Data Sourced
via NVD·05:18 AM
DescriptionSeverityWeakness
Sep 24, 2026
Advisory Published
via GitHub·07:50 PM
Data Sourced
via GitHub·07:50 PM
DescriptionSeverityWeaknessAffected Software
Oct 14, 2026
Data Sourced
via IBM·12:00 AM
DescriptionAffected Software

Parent advisories

This vulnerability appears in the following advisories.

Frequently Asked Questions

1

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.

2

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.

3

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.

4

What version resolves the issue?

The issue is fixed in Trestle version 4.1.0.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203