Skip to content

Commit 2dd1cce

Browse files
authored
Merge commit from fork
* fix: jinja write Signed-off-by: degenaro <lou.degenaro@gmail.com> * fix: improve validation Signed-off-by: degenaro <lou.degenaro@gmail.com> --------- Signed-off-by: degenaro <lou.degenaro@gmail.com>
1 parent 53de5e7 commit 2dd1cce

2 files changed

Lines changed: 83 additions & 3 deletions

File tree

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
import pathlib
1818
import shutil
1919

20+
import pytest
21+
2022
from _pytest.monkeypatch import MonkeyPatch
2123

2224
from tests.test_utils import execute_command_and_assert, setup_for_ssp
2325

26+
from trestle.common.err import TrestleError
2427
from trestle.core.commands.author.jinja import _number_captions
2528
from trestle.core.commands.author.ssp import SSPGenerate
2629
from trestle.core.markdown.docs_markdown_node import DocsMarkdownNode
@@ -295,3 +298,69 @@ def test_jinja_with_template_only(
295298
node1 = tree.get_node_for_key('# A')
296299
node2 = tree.get_node_for_key('# C')
297300
assert node1.subnodes[0].key == node2.subnodes[0].key
301+
302+
303+
def test_jinja_path_traversal_protection(
304+
testdata_dir: pathlib.Path, tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch
305+
) -> None:
306+
"""Test that path traversal attacks are blocked in jinja command."""
307+
from trestle.core.remote.security import PathSecurityValidator
308+
309+
# Test path validation directly to ensure 100% coverage of the validation code
310+
# Test 1: Path traversal with ../ should fail
311+
with pytest.raises(TrestleError) as exc_info:
312+
output_file = tmp_trestle_dir / '../../../etc/passwd'
313+
PathSecurityValidator.validate_local_path(output_file, tmp_trestle_dir)
314+
assert 'Security violation' in str(exc_info.value)
315+
assert 'Path traversal blocked' in str(exc_info.value)
316+
317+
# Test 2: Path traversal with multiple ../ should fail
318+
with pytest.raises(TrestleError) as exc_info:
319+
output_file = tmp_trestle_dir / 'subdir/../../poc.txt'
320+
PathSecurityValidator.validate_local_path(output_file, tmp_trestle_dir)
321+
assert 'Security violation' in str(exc_info.value)
322+
323+
# Test 3: Absolute path should fail
324+
with pytest.raises(TrestleError) as exc_info:
325+
output_file = pathlib.Path('/tmp/attack.md')
326+
PathSecurityValidator.validate_local_path(output_file, tmp_trestle_dir)
327+
assert 'Security violation' in str(exc_info.value)
328+
329+
# Test 4: Complex traversal should fail
330+
with pytest.raises(TrestleError) as exc_info:
331+
output_file = tmp_trestle_dir / 'a/b/c/../../../../etc/passwd'
332+
PathSecurityValidator.validate_local_path(output_file, tmp_trestle_dir)
333+
assert 'Security violation' in str(exc_info.value)
334+
335+
# Test 5: Valid relative path should succeed
336+
output_file = tmp_trestle_dir / 'output/valid.md'
337+
PathSecurityValidator.validate_local_path(output_file, tmp_trestle_dir) # Should not raise
338+
339+
340+
def test_jinja_docs_profile_path_traversal_protection(tmp_trestle_dir: pathlib.Path) -> None:
341+
"""Test that path traversal attacks are blocked in jinja docs-profile mode."""
342+
from trestle.core.remote.security import PathSecurityValidator
343+
344+
# Test validation for multi-file output paths
345+
# Test 1: Path traversal in output directory should fail
346+
with pytest.raises(TrestleError) as exc_info:
347+
output_file = tmp_trestle_dir / '../../../etc/ac-1.md'
348+
PathSecurityValidator.validate_local_path(output_file, tmp_trestle_dir)
349+
assert 'Security violation' in str(exc_info.value)
350+
assert 'Path traversal blocked' in str(exc_info.value)
351+
352+
# Test 2: Complex path traversal should fail
353+
with pytest.raises(TrestleError) as exc_info:
354+
output_file = tmp_trestle_dir / 'controls/../../tmp/ac-1.md'
355+
PathSecurityValidator.validate_local_path(output_file, tmp_trestle_dir)
356+
assert 'Security violation' in str(exc_info.value)
357+
358+
# Test 3: Directory creation path traversal should fail
359+
with pytest.raises(TrestleError) as exc_info:
360+
group_dir = tmp_trestle_dir / '../../../etc/malicious'
361+
PathSecurityValidator.validate_local_path(group_dir, tmp_trestle_dir)
362+
assert 'Security violation' in str(exc_info.value)
363+
364+
# Test 4: Valid relative path should succeed
365+
output_file = tmp_trestle_dir / 'controls_output/ac/ac-1.md'
366+
PathSecurityValidator.validate_local_path(output_file, tmp_trestle_dir) # Should not raise

trestle/core/commands/author/jinja.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from trestle.common import const, log
3131
from trestle.common.err import TrestleIncorrectArgsError, handle_generic_command_exception
3232
from trestle.common.load_validate import load_validate_model_name
33+
from trestle.core.remote.security import PathSecurityValidator
3334
from trestle.common.model_utils import ModelUtils
3435
from trestle.core.catalog.catalog_interface import CatalogInterface
3536
from trestle.core.commands.command_docs import CommandPlusDocs
@@ -228,7 +229,10 @@ def jinja_ify(
228229

229230
output = JinjaCmd.render_template(template, lut, template_folder)
230231

232+
# Validate output path to prevent path traversal
231233
output_file = trestle_root / r_output_file
234+
PathSecurityValidator.validate_local_path(output_file, trestle_root)
235+
232236
if number_captions:
233237
output_file.open('w', encoding=const.FILE_ENCODING).write(_number_captions(output))
234238
else:
@@ -274,8 +278,11 @@ def jinja_multiple_md(
274278
control_path = catalog_interface.get_control_path(control.id)
275279
for sub_dir in control_path:
276280
group_dir = group_dir / sub_dir
277-
if not group_dir.exists():
278-
group_dir.mkdir(parents=True, exist_ok=True)
281+
# Validate directory path to prevent path traversal before creating directories
282+
full_group_dir = trestle_root / group_dir
283+
PathSecurityValidator.validate_local_path(full_group_dir, trestle_root)
284+
if not full_group_dir.exists():
285+
full_group_dir.mkdir(parents=True, exist_ok=True)
279286

280287
control_writer = DocsControlWriter()
281288

@@ -291,7 +298,11 @@ def jinja_multiple_md(
291298
lut['group_title'] = group_title
292299
output = JinjaCmd.render_template(template, lut, template_folder)
293300

294-
output_file = trestle_root / group_dir / pathlib.Path(control.id + const.MARKDOWN_FILE_EXT)
301+
# Validate output path to prevent path traversal
302+
relative_output_path = group_dir / pathlib.Path(control.id + const.MARKDOWN_FILE_EXT)
303+
output_file = trestle_root / relative_output_path
304+
PathSecurityValidator.validate_local_path(output_file, trestle_root)
305+
295306
output_file.open('w', encoding=const.FILE_ENCODING).write(output)
296307

297308
return CmdReturnCodes.SUCCESS.value

0 commit comments

Comments
 (0)