Summary
Trestle has Server-Side Template Injection (SSTI) via Recursive Template Re-evaluation of Untrusted Data
Advisory details
Impact
A Server-Side Template Injection (SSTI) vulnerability exists in multiple locations of trestle's Jinja2 rendering pipeline due to a systemic pattern: untrusted data is re-parsed as Jinja2 template source code without sandboxing. This advisory tracks the root cause across all affected code paths.
The core anti-pattern is: treating runtime data (rendered output, included Markdown content, LUT values) as Jinja2 template source code and passing it to Parser.parse() or an equivalent rendering cycle, without using SandboxedEnvironment or escaping Jinja2 syntax delimiters. Because jinja2.Environment (not SandboxedEnvironment) is used, injected expressions can traverse Python object chains (__class__.__mro__, __globals__, __subclasses__()) to achieve arbitrary command execution via os.system() or subprocess.
Previously fixed instance (historical context):
An earlier version of render_template() in trestle/core/commands/author/jinja.py implemented a recursive while loop: rendered output was loaded via DictLoader into a new Environment and re-rendered until convergence. This allowed an attacker to inject {{ namespace.__init__.__globals__.os.system('command') }} into SSP data fields or LUT YAML values. When a trusted template rendered these data fields (e.g., Title: {{ ssp.metadata.title }}), the injected payload was written into the output, then re-evaluated as executable Jinja2 code in the next loop iteration. **This specific code path was fixed — render_template() now performs a single template.render(**lut) call.
Still-vulnerable code paths (this advisory):
MDCleanInclude.parse()—trestle/core/jinja/tags.py:148-151: Markdown file content is loaded viaFileSystemLoader.get_source(), then re-parsed as Jinja2 source viaParser(self.environment, content).parse().MDSectionInclude.parse()—trestle/core/jinja/tags.py:100-103: Extracted Markdown section text (md_section.content.raw_text) is re-parsed as Jinja2 source viaParser(self.environment, raw_text).parse().MDDatestamp.parse()—trestle/core/jinja/tags.py:198-201: Date string is re-parsed; lower risk because the date string is internally generated fromstrftime()rather than user input.
All three paths share the identical root cause: data that should be treated as plain text is passed to Parser.parse() and executed as Jinja2 code in an un-sandboxed Environment.
Attack vectors:
- Path A (Markdown include): Attacker places a malicious
.mdfile with embedded Jinja2 payload in the trestle workspace. When{% md_clean_include "malicious.md" %}or{% mdsection_include %}is processed, the payload executes. - Path B (Data field injection — SSP/LUT): Attacker crafts an SSP document or YAML LUT where a data field value (e.g.,
metadata.title) contains{{ namespace.__init__.__globals__.os.system('id') }}. When rendered into a trusted template, if the output subsequently flows through any re-parsing code path, the payload executes.
The same __globals__.os.system() RCE technique demonstrated in the previously-fixed render_template vulnerability applies to the remaining re-parsing paths.
Workarounds
- Disable vulnerable tags: Remove
MDCleanIncludeandMDSectionIncludefrom the Jinja2 extensions list intrestle/core/jinja/ext.py:32if markdown includes are not required. - Audit included Markdown files: Review all Markdown files referenced by
{% md_clean_include %}and{% mdsection_include %}tags for unexpected Jinja2 syntax ({{ }},{% %},{# #}). - Scan data sources: Scan SSP documents, YAML LUT files, and any other data sources rendered into templates for Jinja2 syntax patterns.
- Restrict workspace write access: Ensure only trusted users can add or modify files in trestle workspace directories.
- Pre-commit hook: Add a pre-commit hook to scan
.md,.json,.yamlfiles for Jinja2 syntax patterns ({{ namespace,{% for,__globals__,__class__,__mro__,__subclasses__,os.system,subprocess) and block commits containing them. - CI/CD isolation: If trestle is used in automated pipelines processing third-party vendor-supplied SSPs or data, run it in an isolated container/sandbox with minimal privileges and no network access.
Attack Path (Validation Evidence)
Path A: via {% md_clean_include %} tag
[Entry Point] CLI: trestle jinja -i template.md.jinja -o output.md
↓ main() → JinjaCmd._run(args) [trestle/core/commands/author/jinja.py:108]
↓
[Setup] JinjaCmd.jinja_ify(trestle_root, input_path, ...) [jinja.py:178]
↓ jinja_env = JinjaCmd._create_jinja_environment(template_folder) [jinja.py:192]
↓ template = jinja_env.get_template(str(r_input_file)) [jinja.py:193]
↓ output = JinjaCmd.render_template(template, lut, template_folder) [jinja.py:225]
↓
[Render] Jinja2 engine encounters {% md_clean_include "malicious.md" %}
↓
[Tag Handler] MDCleanInclude.parse(parser) [tags.py:115]
↓ markdown_source = "malicious.md" [tags.py:127]
↓ self.environment.loader.get_source(self.environment, "malicious.md") [tags.py:139]
↓ ← Loads file content from workspace directory (no restrictions on content)
↓ frontmatter.loads(md_content) → fm.content [tags.py:140-141]
↓ ← NO SANITIZATION: Markdown body assigned directly to content variable
[SINK] local_parser = Parser(self.environment, content) [tags.py:148]
↓ ← Markdown content parsed as Jinja2 template SOURCE CODE
[SINK] top_level_output = local_parser.parse() [tags.py:149]
↓ ← ALL Jinja2 syntax in the .md file is EXECUTED
[Impact] SSTI — attacker-controlled Jinja2 code executes in template context
Path B: via {% mdsection_include %} tag
[Entry Point] Same as Path A
↓ Jinja2 engine encounters {% mdsection_include "doc.md" "Section Title" %}
↓
[Tag Handler] MDSectionInclude.parse(parser) [tags.py:56]
↓ self.environment.loader.get_source(..., markdown_source.value) [tags.py:82]
↓ DocsMarkdownNode.build_tree_from_markdown(fm.content.split('\n')) [tags.py:86]
↓ full_md.get_node_for_key(section_title.value) → md_section [tags.py:87]
↓ ← Extracts specific section from the markdown document
[SINK] local_parser = Parser(self.environment, md_section.content.raw_text) [tags.py:100]
↓ ← Section raw text parsed as Jinja2 template SOURCE CODE
[SINK] top_level_output = local_parser.parse() [tags.py:101]
↓ ← ALL Jinja2 syntax in the extracted section is EXECUTED
[Impact] SSTI — same impact as Path A, limited to a specific markdown section
Taint Flow (Validation Evidence)
Source: User-supplied .md file in trestle workspace (file system)
Type: Markdown text file
Controllability: FULL — attacker controls entire file content
↓
[Transform 1] FileSystemLoader.get_source() [tags.py:82 or 139]
Reads raw file content as string
✓ SANITIZATION: NONE — any content is loaded
↓
[Transform 2] frontmatter.loads(md_content) [tags.py:83 or 140]
Strips YAML frontmatter, preserves Markdown body
✓ SANITIZATION: NONE — only processes YAML header, ignores body content
↓
[Transform 3] fm.content → content variable [tags.py:141] (Path A)
OR md_section.content.raw_text [tags.py:100] (Path B)
Direct string assignment
✓ SANITIZATION: NONE — no filtering, encoding, or validation
↓
[Transform 4] adjust_heading_level(content, expected) [tags.py:146] (Path A only)
Adjusts Markdown heading levels (e.g., ## → ###)
✓ SANITIZATION: NONE — only modifies '#' character count, does not touch Jinja2 syntax
↓
[Sink] Parser(self.environment, tainted_string) [tags.py:100 or 148]
Tainted Markdown content is passed to Jinja2 Parser constructor as template source
[Sink] local_parser.parse() [tags.py:101 or 149]
All Jinja2 constructs ({{ }}, {% %}, {# #}) in tainted content are executed
↓
[Impact] SSTI — Jinja2 co
References
- https://github.com/advisories/GHSA-jw39-3688-r4rx
- https://github.com/oscal-compass/compliance-trestle/security/advisories/GHSA-jw39-3688-r4rx
- https://nvd.nist.gov/vuln/detail/CVE-2026-54757
- https://github.com/oscal-compass/compliance-trestle/pull/2257
- https://github.com/oscal-compass/compliance-trestle/commit/0f82d19bd42f9cc0f1b3acd7fc3f6dafe3b6ae10
- https://github.com/oscal-compass/compliance-trestle/commit/5335ff873a2a68eb7de43df029bea09cadff22fd
- https://github.com/oscal-compass/compliance-trestle/releases/tag/v3.12.4
- https://github.com/oscal-compass/compliance-trestle/releases/tag/v4.1.0
Related vulnerabilities
All Supply chain →- HIGHCVE-2026-75911
CodeWhale: Project config `allow_shell` override enables arbitrary shell command execution via cloned repository
- HIGHCVE-2026-75858
CodeWhale: rlm_eval auto-approves arbitrary Python execution, bypassing the user's approval policy (RCE)
- CRITICALCVE-2026-62681
Orval: RCE via OpenAPI path -> unescaped request-URL template literal (backtick breakout)
- CRITICALCVE-2026-62682
Orval: RCE via servers[].url -> unescaped request-URL template literal (with getBaseUrlFromSpecification)
- CRITICALCVE-2026-72717
Orval: Import-time RCE via schema default -> zod module-level template literal
- CRITICALCVE-2026-71869
Orval: Import-time RCE via array-items default -> zod module-level template literal