From aa40aa90544efd826c44bd7466d35cc12d4ce830 Mon Sep 17 00:00:00 2001 From: dfgvaetyj3456356-hash Date: Thu, 28 May 2026 02:01:47 -0500 Subject: [PATCH 1/2] security: fix path traversal in BaseTuner directory/project_name/tuner_id Fixes a path traversal vulnerability where user-supplied directory, project_name, and tuner_id parameters were joined into filesystem paths without validation. An attacker could set directory='../evil' or project_name='../../etc' to escape the intended project directory. When combined with overwrite=True, this could lead to arbitrary directory deletion. The fix adds _validate_project_path() and _validate_tuner_id() static methods that reject path traversal sequences (..) and path separators in tuner_id. Tests cover directory traversal, project_name traversal, and tuner_id traversal with environment variable input. --- keras_tuner/engine/base_tuner.py | 34 ++++++++++ keras_tuner/engine/base_tuner_test.py | 96 +++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/keras_tuner/engine/base_tuner.py b/keras_tuner/engine/base_tuner.py index 9bba6b91c..00f762f8a 100644 --- a/keras_tuner/engine/base_tuner.py +++ b/keras_tuner/engine/base_tuner.py @@ -115,6 +115,7 @@ def __init__( # Ops and metadata self.directory = directory or "." self.project_name = project_name or "untitled_project" + self._validate_project_path(self.directory, self.project_name) self.oracle._set_project_dir(self.directory, self.project_name) if overwrite and backend.io.exists(self.project_dir): @@ -122,6 +123,7 @@ def __init__( # To support tuning distribution. self.tuner_id = os.environ.get("KERASTUNER_TUNER_ID", "tuner0") + self._validate_tuner_id(self.tuner_id) # Reloading state. if not overwrite and backend.io.exists(self._get_tuner_fname()): @@ -471,5 +473,37 @@ def get_trial_dir(self, trial_id): utils.create_directory(dirname) return dirname + @staticmethod + def _validate_project_path(directory, project_name): + """Validates that directory and project_name do not contain path traversal. + + Raises: + ValueError: If path traversal sequences or absolute paths are detected. + """ + for name, segment in (("directory", str(directory)), ("project_name", str(project_name))): + if ".." in segment: + raise ValueError( + f"Path traversal is not allowed in {name}. Received: {segment!r}" + ) + # Reject absolute paths in project_name to prevent writing outside CWD + if name == "project_name" and os.path.isabs(segment): + raise ValueError( + f"Absolute paths are not allowed in {name}. Received: {segment!r}" + ) + + @staticmethod + def _validate_tuner_id(tuner_id): + """Validates that tuner_id does not contain path traversal sequences. + + Raises: + ValueError: If path traversal sequences or path separators are detected. + """ + tuner_id_str = str(tuner_id) + if ".." in tuner_id_str or "/" in tuner_id_str or "\\" in tuner_id_str: + raise ValueError( + f"tuner_id cannot contain path separators or traversal sequences. " + f"Received: {tuner_id_str!r}" + ) + def _get_tuner_fname(self): return os.path.join(str(self.project_dir), f"{str(self.tuner_id)}.json") diff --git a/keras_tuner/engine/base_tuner_test.py b/keras_tuner/engine/base_tuner_test.py index b7c137bee..5b90f152b 100644 --- a/keras_tuner/engine/base_tuner_test.py +++ b/keras_tuner/engine/base_tuner_test.py @@ -248,3 +248,99 @@ def _the_func(): _the_func, num_workers=2, wait_for_chief=True ) oracle_client.TIMEOUT = timeout + + + +def test_directory_path_traversal_raises_value_error(tmp_path): + def build_model(hp): + hp.Boolean("a") + + with pytest.raises(ValueError, match="Path traversal"): + gridsearch.GridSearch( + directory="../evil", + hypermodel=build_model, + max_trials=1, + ) + + +def test_project_name_path_traversal_raises_value_error(tmp_path): + def build_model(hp): + hp.Boolean("a") + + with pytest.raises(ValueError, match="Path traversal"): + gridsearch.GridSearch( + directory=tmp_path, + project_name="../../evil", + hypermodel=build_model, + max_trials=1, + ) + + +def test_tuner_id_path_traversal_raises_value_error(tmp_path): + def build_model(hp): + hp.Boolean("a") + + import os + original_tuner_id = os.environ.get("KERASTUNER_TUNER_ID") + try: + os.environ["KERASTUNER_TUNER_ID"] = "../../../evil" + with pytest.raises(ValueError, match="tuner_id"): + gridsearch.GridSearch( + directory=tmp_path, + hypermodel=build_model, + max_trials=1, + ) + finally: + if original_tuner_id is not None: + os.environ["KERASTUNER_TUNER_ID"] = original_tuner_id + else: + os.environ.pop("KERASTUNER_TUNER_ID", None) + + +def test_valid_directory_and_project_name_succeeds(tmp_path): + def build_model(hp): + hp.Boolean("a") + + # These should not raise + tuner = gridsearch.GridSearch( + directory=tmp_path, + project_name="my_project", + hypermodel=build_model, + max_trials=1, + ) + assert tuner.directory == str(tmp_path) + assert tuner.project_name == "my_project" + + +def test_project_name_absolute_path_raises_value_error(tmp_path): + def build_model(hp): + hp.Boolean("a") + + with pytest.raises(ValueError, match="Absolute paths"): + gridsearch.GridSearch( + directory=tmp_path, + project_name="/etc", + hypermodel=build_model, + max_trials=1, + ) + + +def test_tuner_id_forward_slash_raises_value_error(tmp_path): + def build_model(hp): + hp.Boolean("a") + + import os + original_tuner_id = os.environ.get("KERASTUNER_TUNER_ID") + try: + os.environ["KERASTUNER_TUNER_ID"] = "evil/tuner" + with pytest.raises(ValueError, match="tuner_id"): + gridsearch.GridSearch( + directory=tmp_path, + hypermodel=build_model, + max_trials=1, + ) + finally: + if original_tuner_id is not None: + os.environ["KERASTUNER_TUNER_ID"] = original_tuner_id + else: + os.environ.pop("KERASTUNER_TUNER_ID", None) From dbc21dee7ab19ec0482dba736fdb8c9337c1c37e Mon Sep 17 00:00:00 2001 From: dfgvaetyj3456356-hash Date: Sun, 31 May 2026 10:24:46 -0500 Subject: [PATCH 2/2] fix: reject Windows absolute project names --- keras_tuner/engine/base_tuner.py | 11 +++++++++-- keras_tuner/engine/base_tuner_test.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/keras_tuner/engine/base_tuner.py b/keras_tuner/engine/base_tuner.py index 00f762f8a..d03a98323 100644 --- a/keras_tuner/engine/base_tuner.py +++ b/keras_tuner/engine/base_tuner.py @@ -15,6 +15,7 @@ import copy +import ntpath import os import traceback import warnings @@ -485,8 +486,14 @@ def _validate_project_path(directory, project_name): raise ValueError( f"Path traversal is not allowed in {name}. Received: {segment!r}" ) - # Reject absolute paths in project_name to prevent writing outside CWD - if name == "project_name" and os.path.isabs(segment): + if ( + name == "project_name" + and ( + os.path.isabs(segment) + or ntpath.isabs(segment) + or ntpath.splitdrive(segment)[0] + ) + ): raise ValueError( f"Absolute paths are not allowed in {name}. Received: {segment!r}" ) diff --git a/keras_tuner/engine/base_tuner_test.py b/keras_tuner/engine/base_tuner_test.py index 5b90f152b..a627bcf39 100644 --- a/keras_tuner/engine/base_tuner_test.py +++ b/keras_tuner/engine/base_tuner_test.py @@ -325,6 +325,24 @@ def build_model(hp): ) +@pytest.mark.parametrize( + "project_name", ["C:\\Windows", "C:Windows", "\\Windows"] +) +def test_project_name_windows_absolute_path_raises_value_error( + tmp_path, project_name +): + def build_model(hp): + hp.Boolean("a") + + with pytest.raises(ValueError, match="Absolute paths"): + gridsearch.GridSearch( + directory=tmp_path, + project_name=project_name, + hypermodel=build_model, + max_trials=1, + ) + + def test_tuner_id_forward_slash_raises_value_error(tmp_path): def build_model(hp): hp.Boolean("a")