Skip to content

Commit 7918cb4

Browse files
committed
fix: validate local eval path segments
1 parent 9670ce2 commit 7918cb4

5 files changed

Lines changed: 102 additions & 0 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
18+
def validate_path_segment(value: str, field_name: str) -> None:
19+
"""Rejects values that could alter a filesystem path.
20+
21+
Args:
22+
value: The caller-supplied identifier.
23+
field_name: Human-readable field name used in error messages.
24+
25+
Raises:
26+
ValueError: If the value contains path separators, traversal segments, or
27+
null bytes.
28+
"""
29+
if not value:
30+
raise ValueError(f"{field_name} must not be empty.")
31+
if "\x00" in value:
32+
raise ValueError(f"{field_name} must not contain null bytes.")
33+
if "/" in value or "\\" in value:
34+
raise ValueError(
35+
f"{field_name} {value!r} must not contain path separators."
36+
)
37+
if value in (".", ".."):
38+
raise ValueError(
39+
f"{field_name} {value!r} must not contain traversal segments."
40+
)

src/google/adk/evaluation/local_eval_set_results_manager.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from ..errors.not_found_error import NotFoundError
2323
from ._eval_set_results_manager_utils import create_eval_set_result
2424
from ._eval_set_results_manager_utils import parse_eval_set_result_json
25+
from ._path_validation import validate_path_segment
2526
from .eval_result import EvalCaseResult
2627
from .eval_result import EvalSetResult
2728
from .eval_set_results_manager import EvalSetResultsManager
@@ -46,6 +47,8 @@ def save_eval_set_result(
4647
eval_case_results: list[EvalCaseResult],
4748
) -> None:
4849
"""Creates and saves a new EvalSetResult given eval_case_results."""
50+
validate_path_segment(app_name, "app_name")
51+
validate_path_segment(eval_set_id, "eval_set_id")
4952
eval_set_result = create_eval_set_result(
5053
app_name, eval_set_id, eval_case_results
5154
)
@@ -67,6 +70,7 @@ def get_eval_set_result(
6770
self, app_name: str, eval_set_result_id: str
6871
) -> EvalSetResult:
6972
"""Returns an EvalSetResult identified by app_name and eval_set_result_id."""
73+
validate_path_segment(eval_set_result_id, "eval_set_result_id")
7074
# Load the eval set result file data.
7175
maybe_eval_result_file_path = (
7276
os.path.join(
@@ -97,4 +101,5 @@ def list_eval_set_results(self, app_name: str) -> list[str]:
97101
return eval_result_files
98102

99103
def _get_eval_history_dir(self, app_name: str) -> str:
104+
validate_path_segment(app_name, "app_name")
100105
return os.path.join(self._agents_dir, app_name, _ADK_EVAL_HISTORY_DIR)

src/google/adk/evaluation/local_eval_sets_manager.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from ._eval_sets_manager_utils import get_eval_case_from_eval_set
3434
from ._eval_sets_manager_utils import get_eval_set_from_app_and_id
3535
from ._eval_sets_manager_utils import update_eval_case_in_eval_set
36+
from ._path_validation import validate_path_segment
3637
from .eval_case import EvalCase
3738
from .eval_case import IntermediateData
3839
from .eval_case import Invocation
@@ -247,6 +248,7 @@ def list_eval_sets(self, app_name: str) -> list[str]:
247248
Raises:
248249
NotFoundError: If the eval directory for the app is not found.
249250
"""
251+
validate_path_segment(app_name, "app_name")
250252
eval_set_file_path = os.path.join(self._agents_dir, app_name)
251253
eval_sets = []
252254
try:
@@ -310,6 +312,8 @@ def delete_eval_case(
310312
self._save_eval_set(app_name, eval_set_id, updated_eval_set)
311313

312314
def _get_eval_set_file_path(self, app_name: str, eval_set_id: str) -> str:
315+
validate_path_segment(app_name, "app_name")
316+
validate_path_segment(eval_set_id, "eval_set_id")
313317
return os.path.join(
314318
self._agents_dir,
315319
app_name,

tests/unittests/evaluation/test_local_eval_set_results_manager.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,22 @@ def test_save_eval_set_result(self, mocker):
9292
expected_eval_set_result_data = self.eval_set_result.model_dump(mode="json")
9393
assert expected_eval_set_result_data == actual_eval_set_result_data
9494

95+
@pytest.mark.parametrize("app_name", ["", ".", "..", "foo/bar", "foo\\bar"])
96+
def test_save_eval_set_result_rejects_invalid_app_name(self, app_name):
97+
with pytest.raises(ValueError):
98+
self.manager.save_eval_set_result(
99+
app_name, self.eval_set_id, self.eval_case_results
100+
)
101+
102+
@pytest.mark.parametrize(
103+
"eval_set_id", ["", ".", "..", "foo/bar", "foo\\bar"]
104+
)
105+
def test_save_eval_set_result_rejects_invalid_eval_set_id(self, eval_set_id):
106+
with pytest.raises(ValueError):
107+
self.manager.save_eval_set_result(
108+
self.app_name, eval_set_id, self.eval_case_results
109+
)
110+
95111
def test_get_eval_set_result(self, mocker):
96112
mock_time = mocker.patch("time.time")
97113
mock_time.return_value = self.timestamp
@@ -103,6 +119,20 @@ def test_get_eval_set_result(self, mocker):
103119
)
104120
assert retrieved_result == self.eval_set_result
105121

122+
@pytest.mark.parametrize("app_name", ["", ".", "..", "foo/bar", "foo\\bar"])
123+
def test_get_eval_set_result_rejects_invalid_app_name(self, app_name):
124+
with pytest.raises(ValueError):
125+
self.manager.get_eval_set_result(app_name, self.eval_set_result_name)
126+
127+
@pytest.mark.parametrize(
128+
"eval_set_result_id", ["", ".", "..", "foo/bar", "foo\\bar"]
129+
)
130+
def test_get_eval_set_result_rejects_invalid_eval_set_result_id(
131+
self, eval_set_result_id
132+
):
133+
with pytest.raises(ValueError):
134+
self.manager.get_eval_set_result(self.app_name, eval_set_result_id)
135+
106136
def test_get_eval_set_result_double_encoded_legacy(self):
107137
eval_history_dir = os.path.join(
108138
self.agents_dir, self.app_name, _ADK_EVAL_HISTORY_DIR

tests/unittests/evaluation/test_local_eval_sets_manager.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,29 @@ def test_local_eval_sets_manager_create_eval_set_invalid_id(
395395
with pytest.raises(ValueError, match="Invalid Eval Set ID"):
396396
local_eval_sets_manager.create_eval_set(app_name, eval_set_id)
397397

398+
@pytest.mark.parametrize("app_name", ["", ".", "..", "foo/bar", "foo\\bar"])
399+
def test_local_eval_sets_manager_create_eval_set_rejects_invalid_app_name(
400+
self, local_eval_sets_manager, app_name
401+
):
402+
with pytest.raises(ValueError):
403+
local_eval_sets_manager.create_eval_set(app_name, "test_eval_set")
404+
405+
@pytest.mark.parametrize("app_name", ["", ".", "..", "foo/bar", "foo\\bar"])
406+
def test_local_eval_sets_manager_list_eval_sets_rejects_invalid_app_name(
407+
self, local_eval_sets_manager, app_name
408+
):
409+
with pytest.raises(ValueError):
410+
local_eval_sets_manager.list_eval_sets(app_name)
411+
412+
@pytest.mark.parametrize(
413+
"eval_set_id", ["", ".", "..", "foo/bar", "foo\\bar"]
414+
)
415+
def test_local_eval_sets_manager_get_eval_set_rejects_invalid_eval_set_id(
416+
self, local_eval_sets_manager, eval_set_id
417+
):
418+
with pytest.raises(ValueError):
419+
local_eval_sets_manager.get_eval_set("test_app", eval_set_id)
420+
398421
def test_local_eval_sets_manager_create_eval_set_already_exists(
399422
self, local_eval_sets_manager, mocker
400423
):

0 commit comments

Comments
 (0)