Skip to content

Commit c7749c6

Browse files
committed
fix: Remote Code Execution
Signed-off-by: degenaro <lou.degenaro@gmail.com>
1 parent 2dd1cce commit c7749c6

2 files changed

Lines changed: 54 additions & 31 deletions

File tree

tests/trestle/core/commands/author/jinja_cmd_test.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import os
1717
import pathlib
1818
import shutil
19+
from types import SimpleNamespace
1920

2021
import pytest
2122

@@ -24,7 +25,7 @@
2425
from tests.test_utils import execute_command_and_assert, setup_for_ssp
2526

2627
from trestle.common.err import TrestleError
27-
from trestle.core.commands.author.jinja import _number_captions
28+
from trestle.core.commands.author.jinja import JinjaCmd, _number_captions
2829
from trestle.core.commands.author.ssp import SSPGenerate
2930
from trestle.core.markdown.docs_markdown_node import DocsMarkdownNode
3031

@@ -364,3 +365,43 @@ def test_jinja_docs_profile_path_traversal_protection(tmp_trestle_dir: pathlib.P
364365
# Test 4: Valid relative path should succeed
365366
output_file = tmp_trestle_dir / 'controls_output/ac/ac-1.md'
366367
PathSecurityValidator.validate_local_path(output_file, tmp_trestle_dir) # Should not raise
368+
369+
370+
def test_render_template_does_not_recursively_evaluate_untrusted_data(tmp_path: pathlib.Path) -> None:
371+
"""Test that rendered attacker-controlled data is not re-evaluated as Jinja."""
372+
template_path = tmp_path / 'template.j2'
373+
template_path.write_text('Title: {{ ssp.metadata.title }}', encoding='utf-8')
374+
375+
jinja_env = JinjaCmd._create_jinja_environment(tmp_path)
376+
template = jinja_env.get_template(template_path.name)
377+
378+
lut = {
379+
'ssp': SimpleNamespace(
380+
metadata=SimpleNamespace(title="{{ namespace.__init__.__globals__.os.system('touch poc.txt') }}")
381+
)
382+
}
383+
384+
output = JinjaCmd.render_template(template, lut, tmp_path)
385+
386+
assert output.startswith('Title: {{ namespace.__init__.__globals__.os.system(')
387+
assert 'touch poc.txt' in output
388+
assert '{{' in output
389+
assert '}}' in output
390+
assert '&' in output
391+
assert not (tmp_path / 'poc.txt').exists()
392+
393+
394+
def test_render_template_supports_trusted_include(tmp_path: pathlib.Path) -> None:
395+
"""Test that trusted template includes continue to work."""
396+
include_path = tmp_path / 'partial.j2'
397+
include_path.write_text('World', encoding='utf-8')
398+
399+
template_path = tmp_path / 'template.j2'
400+
template_path.write_text("Hello {% include 'partial.j2' %}", encoding='utf-8')
401+
402+
jinja_env = JinjaCmd._create_jinja_environment(tmp_path)
403+
template = jinja_env.get_template(template_path.name)
404+
405+
output = JinjaCmd.render_template(template, {}, tmp_path)
406+
407+
assert output == 'Hello World'

trestle/core/commands/author/jinja.py

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@
2020
import operator
2121
import pathlib
2222
import re
23-
import uuid
2423
from typing import Any, Dict, Optional
2524

26-
from jinja2 import ChoiceLoader, DictLoader, Environment, FileSystemLoader, Template
25+
from jinja2 import Environment, FileSystemLoader, Template
2726

2827
from ruamel.yaml import YAML
2928

@@ -49,8 +48,6 @@
4948
class JinjaCmd(CommandPlusDocs):
5049
"""Transform an input template to an output document using jinja templating."""
5150

52-
max_recursion_depth = 2
53-
5451
name = 'jinja'
5552

5653
def _init_arguments(self) -> None:
@@ -192,9 +189,7 @@ def jinja_ify(
192189
) -> int:
193190
"""Run jinja over an input file with additional booleans."""
194191
template_folder = pathlib.Path.cwd()
195-
jinja_env = Environment(
196-
loader=FileSystemLoader(template_folder), extensions=extensions(), trim_blocks=True, autoescape=True
197-
)
192+
jinja_env = JinjaCmd._create_jinja_environment(template_folder)
198193
template = jinja_env.get_template(str(r_input_file))
199194
# create boolean dict
200195
if operator.xor(bool(ssp), bool(profile)):
@@ -286,9 +281,7 @@ def jinja_multiple_md(
286281

287282
control_writer = DocsControlWriter()
288283

289-
jinja_env = Environment(
290-
loader=FileSystemLoader(template_folder), extensions=extensions(), trim_blocks=True, autoescape=True
291-
)
284+
jinja_env = JinjaCmd._create_jinja_environment(template_folder)
292285
template = jinja_env.get_template(str(r_input_file))
293286
lut['catalog_interface'] = catalog_interface
294287
lut['control_interface'] = ControlInterface()
@@ -308,27 +301,16 @@ def jinja_multiple_md(
308301
return CmdReturnCodes.SUCCESS.value
309302

310303
@staticmethod
311-
def render_template(template: Template, lut: Dict[str, Any], template_folder: pathlib.Path) -> str:
312-
"""Render template."""
313-
new_output = template.render(**lut)
314-
output = ''
315-
# This recursion allows nesting within expressions (e.g. an expression can contain jinja templates).
316-
error_countdown = JinjaCmd.max_recursion_depth
317-
while new_output != output and error_countdown > 0:
318-
error_countdown = error_countdown - 1
319-
output = new_output
320-
random_name = uuid.uuid4() # Should be random and not used.
321-
dict_loader = DictLoader({str(random_name): new_output})
322-
jinja_env = Environment(
323-
loader=ChoiceLoader([dict_loader, FileSystemLoader(template_folder)]),
324-
extensions=extensions(),
325-
autoescape=True,
326-
trim_blocks=True,
327-
)
328-
template = jinja_env.get_template(str(random_name))
329-
new_output = template.render(**lut)
304+
def _create_jinja_environment(template_folder: pathlib.Path) -> Environment:
305+
"""Create the trusted Jinja environment used for loading template files."""
306+
return Environment(
307+
loader=FileSystemLoader(template_folder), extensions=extensions(), trim_blocks=True, autoescape=True
308+
)
330309

331-
return output
310+
@staticmethod
311+
def render_template(template: Template, lut: Dict[str, Any], template_folder: pathlib.Path) -> str:
312+
"""Render a trusted template exactly once to avoid recursive SSTI of untrusted data."""
313+
return template.render(**lut)
332314

333315

334316
def _number_captions(md_body: str) -> str:

0 commit comments

Comments
 (0)