From a893fa052a00d6e9628135f4665108da5e5abf21 Mon Sep 17 00:00:00 2001 From: Shivan Date: Fri, 27 Mar 2026 23:07:06 -0700 Subject: [PATCH 1/3] fix(evaluation): Prevent path traversal in local eval managers This commit adds a strict validation regex (^[a-zA-Z0-9_\-\.]+$) and explicit `..` checks for app_name, eval_set_id, eval_case_id, and eval_set_result_id in LocalEvalSetsManager and LocalEvalSetResultsManager. By sanitizing path parameters, this prevents directory traversal attacks when the FastAPI endpoints attempt to read or modify evaluation JSON files on the local filesystem. --- .../evaluation/local_eval_set_results_manager.py | 10 ++++++++++ .../adk/evaluation/local_eval_sets_manager.py | 14 ++++++++------ .../test_local_eval_set_results_manager.py | 8 ++++++++ .../evaluation/test_local_eval_sets_manager.py | 11 ++++++++++- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/google/adk/evaluation/local_eval_set_results_manager.py b/src/google/adk/evaluation/local_eval_set_results_manager.py index c6da638abe..656d9f411e 100644 --- a/src/google/adk/evaluation/local_eval_set_results_manager.py +++ b/src/google/adk/evaluation/local_eval_set_results_manager.py @@ -16,6 +16,7 @@ import logging import os +import re from typing_extensions import override @@ -67,6 +68,7 @@ def get_eval_set_result( self, app_name: str, eval_set_result_id: str ) -> EvalSetResult: """Returns an EvalSetResult identified by app_name and eval_set_result_id.""" + self._validate_id("Eval Set Result ID", eval_set_result_id) # Load the eval set result file data. maybe_eval_result_file_path = ( os.path.join( @@ -97,4 +99,12 @@ def list_eval_set_results(self, app_name: str) -> list[str]: return eval_result_files def _get_eval_history_dir(self, app_name: str) -> str: + self._validate_id("App Name", app_name) return os.path.join(self._agents_dir, app_name, _ADK_EVAL_HISTORY_DIR) + + def _validate_id(self, id_name: str, id_value: str): + pattern = r"^[a-zA-Z0-9_\-\.]+$" + if not bool(re.fullmatch(pattern, id_value)) or ".." in id_value: + raise ValueError( + f"Invalid {id_name}. {id_name} should have the `{pattern}` format and not contain `..`", + ) diff --git a/src/google/adk/evaluation/local_eval_sets_manager.py b/src/google/adk/evaluation/local_eval_sets_manager.py index 8d2290b911..3f2f0ca77f 100644 --- a/src/google/adk/evaluation/local_eval_sets_manager.py +++ b/src/google/adk/evaluation/local_eval_sets_manager.py @@ -201,7 +201,7 @@ def get_eval_set(self, app_name: str, eval_set_id: str) -> Optional[EvalSet]: try: eval_set_file_path = self._get_eval_set_file_path(app_name, eval_set_id) return load_eval_set_from_file(eval_set_file_path, eval_set_id) - except FileNotFoundError: + except (FileNotFoundError, ValueError): return None @override @@ -211,8 +211,6 @@ def create_eval_set(self, app_name: str, eval_set_id: str) -> EvalSet: Raises: ValueError: If Eval Set ID is not valid or an eval set already exists. """ - self._validate_id(id_name="Eval Set ID", id_value=eval_set_id) - # Define the file path new_eval_set_path = self._get_eval_set_file_path(app_name, eval_set_id) @@ -247,6 +245,7 @@ def list_eval_sets(self, app_name: str) -> list[str]: Raises: NotFoundError: If the eval directory for the app is not found. """ + self._validate_id("App Name", app_name) eval_set_file_path = os.path.join(self._agents_dir, app_name) eval_sets = [] try: @@ -266,6 +265,7 @@ def get_eval_case( self, app_name: str, eval_set_id: str, eval_case_id: str ) -> Optional[EvalCase]: """Returns an EvalCase if found; otherwise, None.""" + self._validate_id("Eval Case ID", eval_case_id) eval_set = self.get_eval_set(app_name, eval_set_id) if not eval_set: return None @@ -310,6 +310,8 @@ def delete_eval_case( self._save_eval_set(app_name, eval_set_id, updated_eval_set) def _get_eval_set_file_path(self, app_name: str, eval_set_id: str) -> str: + self._validate_id("App Name", app_name) + self._validate_id("Eval Set ID", eval_set_id) return os.path.join( self._agents_dir, app_name, @@ -317,10 +319,10 @@ def _get_eval_set_file_path(self, app_name: str, eval_set_id: str) -> str: ) def _validate_id(self, id_name: str, id_value: str): - pattern = r"^[a-zA-Z0-9_]+$" - if not bool(re.fullmatch(pattern, id_value)): + pattern = r"^[a-zA-Z0-9_\-\.]+$" + if not bool(re.fullmatch(pattern, id_value)) or ".." in id_value: raise ValueError( - f"Invalid {id_name}. {id_name} should have the `{pattern}` format", + f"Invalid {id_name}. {id_name} should have the `{pattern}` format and not contain `..`", ) def _write_eval_set_to_path(self, eval_set_path: str, eval_set: EvalSet): diff --git a/tests/unittests/evaluation/test_local_eval_set_results_manager.py b/tests/unittests/evaluation/test_local_eval_set_results_manager.py index 4647392628..5b2c873e29 100644 --- a/tests/unittests/evaluation/test_local_eval_set_results_manager.py +++ b/tests/unittests/evaluation/test_local_eval_set_results_manager.py @@ -174,3 +174,11 @@ def test_list_eval_set_results_empty(self): # No eval set results saved for the app results = self.manager.list_eval_set_results(self.app_name) assert results == [] + + def test_get_eval_history_dir_invalid_app_name(self): + with pytest.raises(ValueError, match="Invalid App Name"): + self.manager.list_eval_set_results("../invalid") + + def test_get_eval_set_result_invalid_id(self): + with pytest.raises(ValueError, match="Invalid Eval Set Result ID"): + self.manager.get_eval_set_result(self.app_name, "../invalid_id") diff --git a/tests/unittests/evaluation/test_local_eval_sets_manager.py b/tests/unittests/evaluation/test_local_eval_sets_manager.py index 3450fb9338..67e089a3db 100644 --- a/tests/unittests/evaluation/test_local_eval_sets_manager.py +++ b/tests/unittests/evaluation/test_local_eval_sets_manager.py @@ -390,11 +390,20 @@ def test_local_eval_sets_manager_create_eval_set_invalid_id( self, local_eval_sets_manager ): app_name = "test_app" - eval_set_id = "invalid-id" + eval_set_id = "invalid/id" with pytest.raises(ValueError, match="Invalid Eval Set ID"): local_eval_sets_manager.create_eval_set(app_name, eval_set_id) + def test_local_eval_sets_manager_create_eval_set_invalid_app_name( + self, local_eval_sets_manager + ): + app_name = "../test_app" + eval_set_id = "test_eval_set" + + with pytest.raises(ValueError, match="Invalid App Name"): + local_eval_sets_manager.create_eval_set(app_name, eval_set_id) + def test_local_eval_sets_manager_create_eval_set_already_exists( self, local_eval_sets_manager, mocker ): From 6c8a5589de2a671f1f13c123609f6f8495c0fe11 Mon Sep 17 00:00:00 2001 From: shivan4030 <9358527+shivan4030@users.noreply.github.com> Date: Sat, 28 Mar 2026 08:48:52 +0000 Subject: [PATCH 2/3] security: Replace weak MD5 hash with SHA256 in MCPSessionManager Replaces `hashlib.md5` with `hashlib.sha256` for session key generation in `mcp_session_manager.py` to mitigate security risks associated with weak cryptographic hashes. Updated the corresponding unit test to expect SHA256 hashes. --- src/google/adk/tools/mcp_tool/mcp_session_manager.py | 2 +- tests/unittests/tools/mcp_tool/test_mcp_session_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/google/adk/tools/mcp_tool/mcp_session_manager.py b/src/google/adk/tools/mcp_tool/mcp_session_manager.py index 23f14421bd..dfd42bb2ad 100644 --- a/src/google/adk/tools/mcp_tool/mcp_session_manager.py +++ b/src/google/adk/tools/mcp_tool/mcp_session_manager.py @@ -278,7 +278,7 @@ def _generate_session_key( # For SSE and StreamableHTTP connections, use merged headers if merged_headers: headers_json = json.dumps(merged_headers, sort_keys=True) - headers_hash = hashlib.md5(headers_json.encode()).hexdigest() + headers_hash = hashlib.sha256(headers_json.encode()).hexdigest() return f'session_{headers_hash}' else: return 'session_no_headers' diff --git a/tests/unittests/tools/mcp_tool/test_mcp_session_manager.py b/tests/unittests/tools/mcp_tool/test_mcp_session_manager.py index 9b1a8f6b7a..64b5fd09fe 100644 --- a/tests/unittests/tools/mcp_tool/test_mcp_session_manager.py +++ b/tests/unittests/tools/mcp_tool/test_mcp_session_manager.py @@ -265,7 +265,7 @@ def test_generate_session_key_sse(self): # Should be deterministic hash headers_json = json.dumps(headers1, sort_keys=True) - expected_hash = hashlib.md5(headers_json.encode()).hexdigest() + expected_hash = hashlib.sha256(headers_json.encode()).hexdigest() assert key1 == f"session_{expected_hash}" def test_merge_headers_stdio(self): From cce6dd7ff3ef4c775907ffb78d0399f395c956aa Mon Sep 17 00:00:00 2001 From: Shivan Date: Tue, 31 Mar 2026 22:38:03 -0700 Subject: [PATCH 3/3] fix: address formatting and typing issues --- contributing/samples/gepa/experiment.py | 1 - contributing/samples/gepa/run_experiment.py | 1 - src/google/adk/evaluation/local_eval_set_results_manager.py | 3 ++- src/google/adk/evaluation/local_eval_sets_manager.py | 3 ++- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contributing/samples/gepa/experiment.py b/contributing/samples/gepa/experiment.py index f3751206a8..2710c3894c 100644 --- a/contributing/samples/gepa/experiment.py +++ b/contributing/samples/gepa/experiment.py @@ -43,7 +43,6 @@ from tau_bench.types import EnvRunResult from tau_bench.types import RunConfig import tau_bench_agent as tau_bench_agent_lib - import utils diff --git a/contributing/samples/gepa/run_experiment.py b/contributing/samples/gepa/run_experiment.py index d857da9635..e31db15788 100644 --- a/contributing/samples/gepa/run_experiment.py +++ b/contributing/samples/gepa/run_experiment.py @@ -25,7 +25,6 @@ from absl import flags import experiment from google.genai import types - import utils _OUTPUT_DIR = flags.DEFINE_string( diff --git a/src/google/adk/evaluation/local_eval_set_results_manager.py b/src/google/adk/evaluation/local_eval_set_results_manager.py index 656d9f411e..d3d67c6d67 100644 --- a/src/google/adk/evaluation/local_eval_set_results_manager.py +++ b/src/google/adk/evaluation/local_eval_set_results_manager.py @@ -106,5 +106,6 @@ def _validate_id(self, id_name: str, id_value: str): pattern = r"^[a-zA-Z0-9_\-\.]+$" if not bool(re.fullmatch(pattern, id_value)) or ".." in id_value: raise ValueError( - f"Invalid {id_name}. {id_name} should have the `{pattern}` format and not contain `..`", + f"Invalid {id_name}. {id_name} should have the `{pattern}` format and" + " not contain `..`", ) diff --git a/src/google/adk/evaluation/local_eval_sets_manager.py b/src/google/adk/evaluation/local_eval_sets_manager.py index 3f2f0ca77f..0fe1357caa 100644 --- a/src/google/adk/evaluation/local_eval_sets_manager.py +++ b/src/google/adk/evaluation/local_eval_sets_manager.py @@ -322,7 +322,8 @@ def _validate_id(self, id_name: str, id_value: str): pattern = r"^[a-zA-Z0-9_\-\.]+$" if not bool(re.fullmatch(pattern, id_value)) or ".." in id_value: raise ValueError( - f"Invalid {id_name}. {id_name} should have the `{pattern}` format and not contain `..`", + f"Invalid {id_name}. {id_name} should have the `{pattern}` format and" + " not contain `..`", ) def _write_eval_set_to_path(self, eval_set_path: str, eval_set: EvalSet):