> ## Content Index
> Fetch the complete content index at: https://f4n6.co.uk/llms.txt
> Use this file to discover other available public pages before exploring further.

# Trestle SSTI in Jinja2 include tags allows arbitrary code execution (Incomplete fix of CVE-2026-46439)
- URL: https://f4n6.co.uk/security-feed/trestle-ssti-in-jinja2-include-tags-allows-arbitrary-code-execution-incomplete-fix-of-cve-2026-46439/
- Published: 2026-09-24T22:54:48.000Z
- Updated: 2026-09-24T22:54:48.000Z
- Author: Jeff Davies
- Tags: #security-feed, CVE-2026-46439

## 1\. Executive summary

A public advisory published on 2026-09-24 describes a server-side template injection flaw in compliance-trestle that survives the fix for CVE-2026-46439\. The parent flaw is rated CVSS 7.8 HIGH (CWE-94, CWE-1336), is not listed in CISA KEV, and carries an EPSS score of 0%. The new advisory covers the same trust boundary through a sink the parent fix did not touch: the `md_clean_include` and `mdsection_include` Jinja2 tags re-parse the content of an included markdown file as a template inside a non-sandboxed `jinja2.Environment`, so OSCAL prose or component-description fields that reach a generated markdown file can execute code on the process running trestle, which in the documented workflow is a CI runner. No exploitation has been observed, and the reference data assigns no CVSS score to the new advisory. Exposure for EMEA financial services is limited to teams that run trestle in automated pipelines and ingest OSCAL from parties outside their own trust boundary, such as component suppliers or upstream catalog maintainers.

## 2\. Regulatory framing

No specific DORA/NIS2 article is directly engaged by this item.

The item is a vulnerability disclosure with no confirmed exploitation, no incident, and no affected production service named. The involvement of a third-party open-source component is not by itself a trigger for DORA Art. 28 or NIS2 Art. 21(2)(d), and no reporting clock under DORA Art. 19 or NIS2 Art. 23 starts until an incident occurs. If a client later confirms execution of this flaw in a pipeline that supports a regulated function, DORA Art. 17 and Art. 18 become relevant at that point, and the classification decision should be made against the incident's actual impact rather than against this advisory.

## 3\. Technical analysis & attack chain

The affected component is compliance-trestle, an OSCAL toolchain. The advisory states that versions 4.0.3 and earlier are affected on this code path, and that the 3.12.x line is affected in the same way. The reporter verified the flaw by direct source read in 4.0.3, which is the release that contains the fix for the parent CVE. No patched version is named in the source material.

The parent fix removed the recursive re-render loop in `render_template` in `trestle/core/commands/author/jinja.py`. It did not touch the custom include tags in `trestle/core/jinja/tags.py`. Those tags still pass the text of an included file to `Parser(self.environment, <file text>).parse()`, which compiles that text as a Jinja2 template and splices the resulting AST body into the host template's compilation. Any `{{ ... }}` or `{% ... %}` in the included file is therefore evaluated as template code rather than emitted as literal text. The environment is a plain `jinja2.Environment`, not `jinja2.sandbox.SandboxedEnvironment`, so a re-parsed expression reaches the usual Jinja object-traversal chain and from there OS command execution. `autoescape=True` HTML-escapes output but does not restrict expression evaluation.

The two sinks, with the line numbers given in the advisory:

- `MDSectionInclude.parse`, the `{% mdsection_include 'file.md' '# Section' %}` tag. `self.environment.loader.get_source(...)` at approximately line 90, then `Parser(self.environment, md_section.content.raw_text)` at approximately line 104, `local_parser.parse()` at approximately line 105, returning `top_level_output.body` at approximately line 106.
- `MDCleanInclude.parse`, the `{% md_clean_include 'file.md' %}` tag. `get_source` at approximately line 141, then `Parser(self.environment, content)` at approximately line 150, `local_parser.parse()` at approximately line 151, returning `top_level_output.body` at approximately line 152.

A third path, `MDDatestamp.parse` at approximately lines 198 to 201, re-parses a date string. The advisory rates it lower risk because the string is generated internally by `strftime()` rather than from user input.

The data bridge is the trestle markdown writers. `trestle/core/ssp_io.py` (`SSPMarkdownWriter`) writes control statement prose from `get_control_statement_ssp` and component implementation descriptions from `prose = by_comp.description` into markdown without escaping Jinja delimiters. `trestle/core/docs_control_writer.py` (`DocsControlWriter`) writes control section prose through `self._md_file.new_line(prose)` and part text raw. The maintainers already apply `line.replace('{{', '[[').replace('}}', ']]')` in `ssp_io.py`, but only inside the parameter-table method `_parameter_table`. Control statement prose and component description fields receive no such neutralization.

The reachability chain, as reasoned by the reporter and not as observed exploitation:

1. A pipeline ingests OSCAL content (component-definition, SSP, profile, or catalog) that an external or lower-trust party can influence, such as a component supplier, a control-narrative contributor, or an upstream catalog.
2. The pipeline runs `trestle author ssp-generate` or an equivalent generator, which writes the OSCAL prose and description fields into markdown verbatim. A value such as `{{ cycler.__init__.__globals__['os'].popen('id').read() }}` survives intact because no neutralization is applied to prose or description fields.
3. The pipeline runs `trestle author jinja -i template.md.j2 ...` where the template composes the document with `{% md_clean_include 'generated_control.md' %}` or `{% mdsection_include 'generated_ssp.md' '# Some Section' %}`, the documented pattern for populating content from other documents.
4. The include tag re-parses the generated markdown in the non-sandboxed environment and the embedded expression executes with the privileges of the trestle process, which is the CI runner.

The reporter's non-destructive confirmation is to place a benign marker such as `{{ 7*7 }}` in an OSCAL prose or description field, run the generator that writes it to markdown, include that markdown with `{% md_clean_include 'generated.md' %}`, and observe `49` in the output instead of the literal `{{ 7*7 }}`. The malicious gadget chain is deliberately omitted from the advisory.

Related advisories in the source set describe the same root cause across the rendering pipeline. GHSA-jw39-3688-r4rx tracks it as a systemic pattern of untrusted data being re-parsed as Jinja2 source without sandboxing, and lists `MDCleanInclude.parse`, `MDSectionInclude.parse`, and `MDDatestamp.parse` as the still-vulnerable paths. GHSA-gg2g-p7xc-qqmm documents the previously fixed `render_template` recursion, including a proof of concept against Jinja2 3.1.6 in which a payload in the SSP `metadata.title` field is executed by `trestle author jinja -i template.j2 -o out.md -ssp malicious_ssp`. That path is fixed; it is included here as context for the shared trust boundary, not as a live issue.

Two points of uncertainty. First, the reachability chain above is the reporter's reasoned analysis, described in the source as non-destructive reasoning, not a field observation. Second, the advisory is single-sourced to the reporter, Cavan Loughran of Celvex Group Inc., via the GitHub Advisory Database. The code-level claims are specific enough to be checked by reading the named files, and clients who run trestle should do that rather than take the chain on trust.

On classification, the verified reference data records CWE-94 (Code Injection) and CWE-1336 for CVE-2026-46439\. The source describes the class as SSTI leading to RCE. Both describe the same flaw: CWE-1336 is the template-engine neutralization failure and CWE-94 is the resulting code injection. One source, external-1, labels the GHSA URL for this advisory as CVE-2026-57170\. The primary item and the other sources carry no CVE identifier for this advisory. Treat the CVE-2026-57170 label as single-sourced and unconfirmed.

## 4\. Mitigation & containment

No vendor fix is available. The source names 4.0.3 as affected and lists no patched release. The steps below are workarounds and compensating controls.

P1, within 24 hours:

- Inventory every pipeline that invokes `trestle author jinja`. Grep the template directory for `md_clean_include` and `mdsection_include`. If neither tag is used, the exposure is closed for that pipeline.
- Where the include tags are not required, remove `MDCleanInclude` and `MDSectionInclude` from the Jinja2 extensions list in `trestle/core/jinja/ext.py` (line 32 in the version referenced by the source). This is a local modification to an installed or vendored package, so record it and reapply it after any upgrade.
- Treat all OSCAL input that crosses a trust boundary as untrusted. Before rendering, scan `.md`, `.json`, and `.yaml` files in the trestle workspace for `{{`, `}}`, `{%`, `%}`, `{#`, and `#}`. Any hit in a prose, description, or part-text field is a candidate payload.
- Restrict write access to trestle workspace directories to trusted users and CI jobs. An attacker who cannot write into the workspace cannot place a malicious include target.

P2, within 72 hours:

- Run trestle jobs in a container with least privilege, no long-lived cloud credentials, and restricted outbound network access. This bounds the impact of successful execution on the runner rather than preventing it.
- Add a pre-commit or CI gate that fails the build when Jinja2 delimiter patterns appear in `.md`, `.json`, or `.yaml` files entering the workspace.
- Audit existing generated markdown and OSCAL documents for delimiter patterns, and review build logs for rendered output that differs from the source field value.

P3, within 7 days:

- Track the upstream fix. The reporter's durable options are to route the include re-parse through `jinja2.sandbox.SandboxedEnvironment` or to emit included file content as literal text instead of re-parsing it. Extending the `{{` to `[[` neutralization in `ssp_io.py` and `docs_control_writer.py` to all prose, description, and part-text writes is defense in depth, not a fix. Pin to a patched release once one exists.
- If you maintain a fork, apply the sandbox or literal-emission change directly and carry it until upstream ships.

## 5\. Indicators of compromise

No indicators of compromise available in the source material. The sources describe no hashes, domains, IP addresses, file paths, or registry keys associated with exploitation.

The sources do describe observable behaviours that a defender can look for. These are not atomic indicators and are not suitable for automated blocking.

### Behavioural indicators

| behaviour                                                                                                                                            | where to observe                                             | confidence                                                                                           |
| ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| Jinja2 delimiters ({{, }}, {%, %}, {#, #}) present in generated markdown under the trestle workspace after a generator run                           | file content in the trestle workspace and CI build artefacts | high, documented in the source                                                                       |
| A benign marker such as {{ 7\*7 }} placed in an OSCAL prose or description field renders as 49 in the generated output instead of the literal string | rendered markdown output and build logs                      | high, this is the source's stated non-destructive test                                               |
| A shell interpreter spawned as a child of the trestle or Python process during a trestle author jinja run                                            | process creation telemetry on the CI runner                  | medium, inferred from the documented os.system and subprocess gadget chain; not observed in the wild |

## 6\. Detection

The YARA rule below targets the gadget-chain strings documented in the source. It is intended for scanning OSCAL, markdown, and LUT files entering a trestle workspace, not for scanning the trestle package itself.

```yara
rule OSCAL_Jinja2_SSTI_Payload
{
    meta:
        author = "Adverse Trace"
        date = "2026-09-24"
        reference = "https://github.com/advisories/GHSA-mr95-65j8-9mxp"
        description = "Detects Jinja2 SSTI gadget chains in OSCAL, markdown or LUT files processed by compliance-trestle include tags"
    strings:
        $g1 = "__globals__" ascii
        $g2 = "__subclasses__" ascii
        $g3 = "__mro__" ascii
        $g4 = "os.system(" ascii
        $g5 = "subprocess" ascii
        $g6 = "popen(" ascii
        $g7 = "cycler.__init__" ascii
        $g8 = "namespace.__init__" ascii
        $d1 = "{{" ascii
        $d2 = "{%" ascii
    condition:
        filesize < 5MB and
        (1 of ($g*) and 1 of ($d*))
}

```

The Sigma rule below detects the execution stage: a shell interpreter spawned by a process whose command line references trestle. Field names differ between backends; `ParentCommandLine` is available in Sysmon event ID 1 and in several EDR process-creation schemas.

```yaml
title: Shell Interpreter Spawned by compliance-trestle Jinja Render
id: 6f2a1c74-9d3b-4e58-b0a1-7c4e2f9a8d10
status: experimental
description: Detects a shell interpreter spawned as a child of a compliance-trestle process, consistent with SSTI code execution via the md_clean_include or mdsection_include tags
references:

    - https://github.com/advisories/GHSA-mr95-65j8-9mxp
    - https://github.com/advisories/GHSA-jw39-3688-r4rx
author: Adverse Trace
date: 2026/09/24
logsource:
    category: process_creation
detection:
    selection_parent:
        ParentCommandLine|contains: 'trestle'
    selection_child:
        Image|endswith:

            - '\cmd.exe'
            - '\powershell.exe'
            - '\pwsh.exe'
            - '/sh'
            - '/bash'
    condition: selection_parent and selection_child
falsepositives:

    - Trestle templates that invoke a shell by design
    - Build tooling that wraps trestle and shells out for unrelated reasons
level: high

```

## CVE assessment

1 referenced CVE

| CVE                                                                              | CVSS     | Exploited | EPSS | Summary                                                                                                                          |
| -------------------------------------------------------------------------------- | -------- | --------- | ---- | -------------------------------------------------------------------------------------------------------------------------------- |
| [CVE-2026-46439](https://nvd.nist.gov/vuln/detail/CVE-2026-46439?ref=f4n6.co.uk) | 7.8 High | —         | 0%   | compliance-trestle is a tooling platform for managing compliance as code. Versions prior to 3.12.2 and 4.0.3 have a Server-Side… |

## 7\. Sources

- GitHub Security Advisories, "Trestle SSTI in Jinja2 include tags allows arbitrary code execution (Incomplete fix of CVE-2026-46439)", https://github.com/advisories/GHSA-mr95-65j8-9mxp, published 2026-09-24\. Reporter: Cavan Loughran, Celvex Group Inc.
- GitHub Security Advisories, "Trestle has Server-Side Template Injection (SSTI) via Recursive Template Re-evaluation of Untrusted Data", https://github.com/advisories/GHSA-jw39-3688-r4rx, date not stated in source.
- GitHub Security Advisories, "compliance-trestle Vulnerable to Remote Code Execution via Recursive Server-Side Template Injection (SSTI)", https://github.com/advisories/GHSA-gg2g-p7xc-qqmm, date not stated in source.
- GitHub Advisory Database, "CVE-2026-57170", https://github.com/advisories/GHSA-mr95-65j8-9mxp, date not stated in source. Same URL as the primary advisory; the CVE identifier appears only in this source.

## 8\. Adverse Trace position

We assess this as a HIGH-severity code execution flaw on the parent CVE-2026-46439 rating of CVSS 7.8, with the caveat that the reference data assigns no CVSS score to the new advisory itself and that the reachability chain is the reporter's reasoned analysis rather than an observed exploitation. CVE-2026-46439 is not in CISA KEV and carries an EPSS score of 0%, so there is no evidence of exploitation in the wild and no basis for treating this as an emergency for the sector as a whole. The exposure is narrow and specific: it applies to clients that run compliance-trestle in an automated pipeline and feed it OSCAL from a party outside their trust boundary. For those clients the impact is code execution on the CI runner, which in most builds holds deployment credentials and network reach into production. The advisory is single-sourced to the reporter via the GitHub Advisory Database; the code-level claims are checkable by reading `trestle/core/jinja/tags.py` and `trestle/core/jinja/ext.py` in your installed version, and we recommend that check before acting on the workarounds. We will track the upstream fix and the CVE assignment for this advisory, and will issue a follow-up when a patched release is named or when exploitation is reported. Clients who want the workspace scan run against their trestle repositories should raise it with their Adverse Trace contact.

---

[Read the original source →](https://github.com/advisories/GHSA-mr95-65j8-9mxp?ref=f4n6.co.uk)

*Published via PulseTrace — Adverse Trace threat intelligence.*