GitHub Advisoryadvisory · PyPI
GHSA-jw39-3688-r4rx Trestle has Server-Side Template Injection (SSTI) via Recursive Template Re-evaluation of Untrusted Data
HIGH
Official page ↗
- Aliases
- CVE-2026-54757
- Ecosystem
- PyPI
- Packages
- compliance-trestle
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
- Weaknesses
- CWE-94
- Reviewed
- 2026-08-28
Affected packages
• PyPI compliance-trestle: < 3.12.4 → fixed in 3.12.4
• PyPI compliance-trestle: >= 4.0.0, < 4.1.0 → fixed in 4.1.0
Description
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):
1. `MDCleanInclude.parse()` — `trestle/core/jinja/tags.py:148-151`: Markdown file content is loaded via `FileSystemLoader.get_source()`, then re-parsed as Jinja2 source via `Parser(self.environment, content).parse()`.
2. `MDSectionInclude.parse()` — `trestle/core/jinja/tags.py:100-103`: Extracted Markdown section text (`md_section.content.raw_text`) is re-parsed as Jinja2 source via `Parser(self.environment, raw_text).parse()`.
3. `MDDatestamp.parse()` — `trestle/core/jinja/tags.py:198-201`: Date string is re-parsed; lower risk because the date string is internally generated from `strftime()` 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 `.md` file 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
1. Disable vulnerable tags: Remove `MDCleanInclude` and `MDSectionInclude` from the Jinja2 extensions list in `trestle/core/jinja/ext.py:32` if markdown includes are not required.
2. Audit included Markdown files: Review all Markdown files referenced by `{% md_clean_include %}` and `{% mdsection_include %}` tags for unexpected Jinja2 syntax (`{{ }}`, `{% %}`, `{# #}`).
3. Scan data sources: Scan SSP documents, YAML LUT files, and any other data sources rendered into templates for Jinja2 syntax patterns.
4. Restrict workspace write access: Ensure only trusted users can add or modify files in trestle workspace directories.
5. Pre-commit hook: Add a pre-commit hook to scan `.md`, `.json`, `.yaml` files for Jinja2 syntax patterns (`{{ namespace`, `{% for`, `__globals__`, `__class__`, `__mro__`, `__subclasses__`, `os.system`, `subprocess`) and block commits containing them.
6. 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 code from attacker-contr…
Related
Alias · CVE (1)
- CVE-2026-54757 Compliance-trestle (Trestle) is a Python SDK and command-line tool for managing OSCAL compliance documents. vulnerability
Weakness · CWE (1)
- CWE-94 Improper Control of Generation of Code weakness
References
- https://github.com/oscal-compass/compliance-trestle/security/advisories/GHSA-jw39-3688-r4rx web
- https://nvd.nist.gov/vuln/detail/CVE-2026-54757 advisory
- https://github.com/oscal-compass/compliance-trestle/pull/2257 web
- https://github.com/oscal-compass/compliance-trestle/commit/0f82d19bd42f9cc0f1b3acd7fc3f6dafe3b6ae10 web
- https://github.com/oscal-compass/compliance-trestle/commit/5335ff873a2a68eb7de43df029bea09cadff22fd web
- https://github.com/oscal-compass/compliance-trestle package
- https://github.com/oscal-compass/compliance-trestle/releases/tag/v3.12.4 web
- https://github.com/oscal-compass/compliance-trestle/releases/tag/v4.1.0 web