GHSA-jw39-3688-r4rx: Code Injection

Published Aug 28, 2026
·
Updated

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 rendertemplate() 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 — rendertemplate() 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.getsource(), 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 (mdsection.content.rawtext) is re-parsed as Jinja2 source via Parser(self.environment, rawtext).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 {% mdcleaninclude "malicious.md" %} or {% mdsectioninclude %} 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 rendertemplate 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 {% mdcleaninclude %} and {% mdsectioninclude %} 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 {% mdcleaninclude %} 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.jinjaify(trestleroot, inputpath, ...) [jinja.py:178] ↓ jinjaenv = JinjaCmd.createjinjaenvironment(templatefolder) [jinja.py:192] ↓ template = jinjaenv.gettemplate(str(rinputfile)) [jinja.py:193] ↓ output = JinjaCmd.rendertemplate(template, lut, templatefolder) [jinja.py:225] ↓ [Render] Jinja2 engine encounters {% mdcleaninclude "malicious.md" %} ↓ [Tag Handler] MDCleanInclude.parse(parser) [tags.py:115] ↓ markdownsource = "malicious.md" [tags.py:127] ↓ self.environment.loader.getsource(self.environment, "malicious.md") [tags.py:139] ↓ ← Loads file content from workspace directory (no restrictions on content) ↓ frontmatter.loads(mdcontent) → fm.content [tags.py:140-141] ↓ ← NO SANITIZATION: Markdown body assigned directly to content variable [SINK] localparser = Parser(self.environment, content) [tags.py:148] ↓ ← Markdown content parsed as Jinja2 template SOURCE CODE [SINK] topleveloutput = localparser.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 {% mdsectioninclude %} tag

[Entry Point] Same as Path A ↓ Jinja2 engine encounters {% mdsectioninclude "doc.md" "Section Title" %} ↓ [Tag Handler] MDSectionInclude.parse(parser) [tags.py:56] ↓ self.environment.loader.getsource(..., markdownsource.value) [tags.py:82] ↓ DocsMarkdownNode.buildtreefrommarkdown(fm.content.split('\n')) [tags.py:86] ↓ fullmd.getnodeforkey(sectiontitle.value) → mdsection [tags.py:87] ↓ ← Extracts specific section from the markdown document [SINK] localparser = Parser(self.environment, mdsection.content.rawtext) [tags.py:100] ↓ ← Section raw text parsed as Jinja2 template SOURCE CODE [SINK] topleveloutput = localparser.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.getsource() [tags.py:82 or 139] Reads raw file content as string ✓ SANITIZATION: NONE — any content is loaded ↓ [Transform 2] frontmatter.loads(mdcontent) [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 mdsection.content.rawtext [tags.py:100] (Path B) Direct string assignment ✓ SANITIZATION: NONE — no filtering, encoding, or validation ↓ [Transform 4] adjustheadinglevel(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, taintedstring) [tags.py:100 or 148] Tainted Markdown content is passed to Jinja2 Parser constructor as template source [Sink] localparser.parse() [tags.py:101 or 149] All Jinja2 constructs ({{ }}, {% %}, {# #}) in tainted content are executed ↓ [Impact] SSTI — Jinja2 code from attacker-controlled Markdown file executes in template context

Sanitization Verdict: ABSENT - No sandboxing: jinja2.Environment used (not SandboxedEnvironment) - No syntax filtering: Jinja2 delimiters {{, {%, {# are NOT escaped - No content validation: Markdown body is not scanned for template syntax - autoescape=True is irrelevant: only affects HTML output encoding, not code execution

Proof of Concept

Setup

bash Initialize trestle workspace trestle init

Create malicious markdown file with Jinja2 payload cat > malicious.md << 'EOF' --- yamlheader: ignored ---

Compliance Documentation

Testing SSTI vulnerability: Execute command: {{ ssp.class.init.globals.builtins.import('os').popen('whoami').read() }} EOF

Create trigger template cat > trigger.md.jinja << 'EOF' POC: SSTI via mdcleaninclude tag

{% mdcleaninclude "malicious.md" %} EOF

Create a dummy LUT file cat > empty.yaml << 'EOF' lut: apikey: supersecrettoken12345 dbpassword: P@ssw0rd2024 jwtsecret: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 awsaccesskey: AKIAIOSFODNN7EXAMPLE awssecretkey: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY internalapi: https://internal.corp.example.com/api/v2/users EOF

Execution

bash trestle init trestle author jinja -i trigger.md.jinja -o output.md -lut empty.yaml

Observed Output

markdown POC: SSTI via mdcleaninclude tag

Compliance Documentation

Testing SSTI vulnerability: Execute command: root

Expected Result

The 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.

Affected Component

- File: trestle/core/jinja/tags.py - Class: MDCleanInclude (lines 106-151) - Class: MDSectionInclude (lines 47-103) - Function: MDCleanInclude.parse() (line 115), MDSectionInclude.parse() (line 56) - Configuring module: trestle/core/commands/author/jinja.py, method createjinjaenvironment() (line 304) - Dependency: Jinja2 (any version) — the vulnerability is in application code, not the Jinja2 library

Fix Recommendation

Important: The fix for rendertemplate() (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.

Comprehensive Fix Strategy

Step 1 (Root cause fix): Remove all secondary Jinja2 parsing from custom tags where it is not needed:

diff tags.py: MDCleanInclude.parse() — replace lines 148-151: - localparser = Parser(self.environment, content) - topleveloutput = localparser.parse() - return topleveloutput.body + from jinja2 import nodes + return [nodes.Output([nodes.TemplateData(content)])]

diff tags.py: MDSectionInclude.parse() — replace lines 100-103: - localparser = Parser(self.environment, mdsection.content.rawtext) - topleveloutput = localparser.parse() - return topleveloutput.body + from jinja2 import nodes + return [nodes.Output([nodes.TemplateData(mdsection.content.rawtext)])]

Step 2 (Defense in depth): Switch to SandboxedEnvironment in createjinjaenvironment():

diff jinja.py:304-308 — createjinjaenvironment() + from jinja2.sandbox import SandboxedEnvironment - return Environment( + return SandboxedEnvironment( loader=FileSystemLoader(templatefolder), extensions=extensions(), trimblocks=True, autoescape=True )

Step 3 (Input validation): Add validation to reject input data containing Jinja2 syntax:

python jinja.py: add to run() before rendering JINJA2DANGEROUSPATTERNS = [ r'\{\{.globals', r'\{\{.class', r'\{\{.mro', r'\{\{.subclasses', r'\{\{.init', r'\{\{.os\.system', r'\{\{.subprocess', r'\{%\sfor\s', r'\{%\sif\s', ]

def validatedatafield(value: str) -> bool: """Reject data values containing suspicious Jinja2 syntax.""" for pattern in JINJA2DANGEROUSPATTERNS: if re.search(pattern, value): return False return True

Alternative Fix (Milder): Escape Jinja2 syntax in untrusted data

diff # tags.py: before any secondary parsing + import re + def escapejinja2(text: str) -> str: + return re.sub(r'(\{\{|\{%|\{#)', r'\\\1', text) + + content = escapejinja2(content) # apply before Parser() localparser = Parser(self.environment, content)

Affected Software

2 affected componentsFixes available
pip/compliance-trestle>=4.0.0<4.1.0
4.1.0
pip/compliance-trestle<=3.12.3
3.12.4

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. Configuration

    In _create_jinja_environment() switch the Jinja2 environment from jinja2.Environment to jinja2.sandbox.SandboxedEnvironment (defense in depth) so re-parsed template content is sandboxed.

    trestle Jinja2 environment (trestle/core/commands/author/jinja.py _create_jinja_environment) environment_class = SandboxedEnvironment
  4. Configuration

    Implement input validation to reject input data containing Jinja2 syntax patterns ({{ }}, {% %}, {# #}) and/or apply escaping before calling Parser(self.environment, content).parse(): use escape_jinja2() that inserts a backslash before the delimiters so Parser does not treat the content as executable Jinja2 (addresses MDCleanInclude.parse() at tags.py:148-151 and MDSectionInclude.parse() at tags.py:100-103).

    trestle Jinja2 tag rendering (escape/validation added before Parser.parse in trestle/core/jinja/tags.py) Jinja2 syntax escaping/validation = Escape delimiters {{, {%, {# by prefixing with backslash (\{{, \{% , \{#) or reject input containing them
  5. Configuration

    If markdown includes are not needed, remove MDCleanInclude and MDSectionInclude from the Jinja2 extensions list in trestle/core/jinja/ext.py:32 to prevent the affected re-parsing code paths from running.

    trestle Jinja2 extensions (trestle/core/jinja/ext.py:32) enabled_extensions = Remove MDCleanInclude and MDSectionInclude when markdown includes are not required
  6. Compensating control

    If trestle is used in automated pipelines processing third-party SSPs or data, run it in an isolated container/sandbox with minimal privileges and no network access (CI/CD isolation mitigation).

  7. Compensating control

    Add a pre-commit hook to scan .md, .json, and .yaml files for Jinja2 syntax patterns ({{ namespace , {% for , __globals__, __class__, __mro__, __subclasses__, os.system, subprocess) and block commits containing them.

  8. Operational

    Restrict workspace write access so only trusted users can add or modify files in trestle workspace directories (prevents attackers from supplying malicious .md content used by md_clean_include/mdsection_include).

  9. Operational

    Audit and scan all Markdown files referenced by {% md_clean_include %} and {% mdsection_include %}, plus SSP documents, YAML LUT files, and other rendered data sources, for unexpected Jinja2 syntax delimiters {{ }}, {% %}, {# #} and suspicious payloads.

Event History

Aug 28, 2026
Advisory Published
via GitHub·04:15 PM
Data Sourced
via GitHub·04:15 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Who is exposed to exploitation?

Installations of compliance-trestle are exposed where untrusted content can reach its Jinja2 rendering pipeline, including rendered output, included Markdown content, or LUT values. The issue affects multiple code paths sharing the same pattern of reparsing runtime data as template source.

2

What conditions are required for exploitation?

An attacker needs a way to introduce Jinja2 expressions into data that is later reparsed and rendered. The vulnerability is locally exploitable without privileges, but the supplied CVSS vector indicates user interaction is required.

3

What is the potential impact of successful exploitation?

Injected Jinja2 expressions can traverse Python object chains and invoke operating-system command execution mechanisms such as os.system() or subprocess. This can result in arbitrary command execution with high confidentiality, integrity, and availability impact.

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