Skip to content

Commit 763548e

Browse files
authored
Merge commit from fork
Signed-off-by: degenaro <lou.degenaro@gmail.com>
1 parent 9698d7e commit 763548e

8 files changed

Lines changed: 39 additions & 1 deletion

File tree

tests/trestle/utils/fs_test.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,13 @@ def test_is_hidden_windows(tmp_path) -> None:
621621
('component-definitions', False),
622622
('hello.world', False),
623623
('component-definitions/hello', False),
624+
# Path traversal attack vectors (CVE-2026-46345 fix)
625+
('/tmp/pwned', False), # Absolute path
626+
('/etc/passwd', False), # Absolute path
627+
('../../tmp/pwned', False), # Leading .. traversal
628+
('subdir/../../../tmp/pwned', False), # Non-leading .. traversal
629+
('subdir/../../etc/passwd', False), # Non-leading .. traversal
630+
('normal/../path', False), # Any .. component
624631
],
625632
)
626633
def test_allowed_task_name(task_name: str, outcome: bool) -> None:

trestle/common/file_utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,17 @@ def is_directory_name_allowed(name: str) -> bool:
105105
# Task must not self-interfere with a project
106106
pathed_name = pathlib.Path(name)
107107

108+
# Defense-in-depth: reject absolute paths
109+
# Check both is_absolute() and if path starts with '/' to handle Unix-style paths on Windows
110+
if pathed_name.is_absolute() or name.startswith('/'):
111+
logger.warning('Task name must not be an absolute path')
112+
return False
113+
114+
# Defense-in-depth: reject any path containing ".." components
115+
if '..' in pathed_name.parts:
116+
logger.warning('Task name must not contain ".." path traversal sequences')
117+
return False
118+
108119
root_path = pathed_name.parts[0]
109120
if root_path in const.MODEL_TYPE_TO_MODEL_DIR.values():
110121
logger.warning('Task name is the same as an OSCAL schema name.')

trestle/core/commands/author/catalog.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from trestle.core.commands.common.return_codes import CmdReturnCodes
3535
from trestle.core.control_context import ContextPurpose, ControlContext
3636
from trestle.core.models.file_content_type import FileContentType
37+
from trestle.core.remote.security import PathSecurityValidator
3738
from trestle.oscal import OSCAL_VERSION
3839
from trestle.oscal.catalog import Catalog
3940

@@ -89,6 +90,9 @@ def _run(self, args: argparse.Namespace) -> int:
8990

9091
markdown_path = trestle_root / args.output
9192

93+
# Validate output path to prevent path traversal
94+
PathSecurityValidator.validate_local_path(markdown_path, trestle_root)
95+
9296
return self.generate_markdown(
9397
trestle_root, catalog_path, markdown_path, yaml_header, args.overwrite_header_values
9498
)

trestle/core/commands/author/headers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ def validate(
315315
# Files in the root directory must be exclused
316316
if path.is_file():
317317
continue
318-
if not file_utils.is_directory_name_allowed(path):
318+
if not file_utils.is_directory_name_allowed(str(relative_path)):
319319
continue
320320
if str(relative_path).rstrip('/') in const.MODEL_DIR_LIST:
321321
continue

trestle/core/commands/author/prof.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from trestle.common.load_validate import load_validate_model_name
4444
from trestle.common.model_utils import ModelUtils
4545
from trestle.core.catalog.catalog_api import CatalogAPI
46+
from trestle.core.remote.security import PathSecurityValidator
4647
from trestle.core.commands.author.common import AuthorCommonCommand
4748
from trestle.core.commands.common.cmd_utils import clear_folder
4849
from trestle.core.commands.common.return_codes import CmdReturnCodes
@@ -109,6 +110,9 @@ def _run(self, args: argparse.Namespace) -> int:
109110

110111
markdown_path = trestle_root / args.output
111112

113+
# Validate output path to prevent path traversal
114+
PathSecurityValidator.validate_local_path(markdown_path, trestle_root)
115+
112116
return self.generate_markdown(
113117
trestle_root,
114118
profile_path,

trestle/core/commands/author/ssp.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from trestle.core.catalog.catalog_reader import CatalogReader
3939
from trestle.core.commands.author.common import AuthorCommonCommand
4040
from trestle.core.commands.author.component import ComponentAssemble
41+
from trestle.core.remote.security import PathSecurityValidator
4142
from trestle.core.commands.common.cmd_utils import clear_folder
4243
from trestle.core.commands.common.return_codes import CmdReturnCodes
4344
from trestle.core.control_context import ContextPurpose, ControlContext
@@ -110,6 +111,9 @@ def _run(self, args: argparse.Namespace) -> int:
110111

111112
md_path = trestle_root / args.output
112113

114+
# Validate output path to prevent path traversal
115+
PathSecurityValidator.validate_local_path(md_path, trestle_root)
116+
113117
return self._generate_ssp_markdown(
114118
trestle_root,
115119
args.profile,

trestle/core/commands/create.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from trestle.common.model_utils import ModelUtils
2929
from trestle.core import generators
3030
from trestle.core.commands.add import Add
31+
from trestle.core.remote.security import PathSecurityValidator
3132
from trestle.core.commands.command_docs import CommandPlusDocs
3233
from trestle.core.commands.common.return_codes import CmdReturnCodes
3334
from trestle.core.models.actions import CreatePathAction, WriteFileAction
@@ -94,6 +95,9 @@ def create_object(cls, model_alias: str, object_type: Type[TopLevelOscalModel],
9495

9596
desired_model_dir = trestle_root / plural_path / args.output
9697

98+
# Validate output path to prevent path traversal
99+
PathSecurityValidator.validate_local_path(desired_model_dir, trestle_root)
100+
97101
desired_model_path = desired_model_dir / (model_alias + '.' + args.extension)
98102

99103
if desired_model_path.exists():

trestle/core/commands/replicate.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from trestle.core.commands.command_docs import CommandPlusDocs
2525
from trestle.core.commands.common.return_codes import CmdReturnCodes
2626
from trestle.core.models.actions import CreatePathAction, WriteFileAction
27+
from trestle.core.remote.security import PathSecurityValidator
2728
from trestle.core.models.elements import Element
2829
from trestle.core.models.file_content_type import FileContentType
2930
from trestle.core.models.plans import Plan
@@ -90,6 +91,9 @@ def replicate_object(cls, model_alias: str, args: argparse.Namespace) -> int:
9091
trestle_root / plural_path / args.output / (model_alias + FileContentType.to_file_extension(content_type))
9192
)
9293

94+
# Validate output path to prevent path traversal
95+
PathSecurityValidator.validate_local_path(rep_model_path, trestle_root)
96+
9397
if rep_model_path.exists():
9498
raise TrestleError(f'OSCAL file to be replicated here: {rep_model_path} exists.')
9599

0 commit comments

Comments
 (0)