Skip to content

Commit 96e845e

Browse files
sasha-gitgcopybara-github
authored andcommitted
fix: enforce allowed file extensions for GET requests in the builder API
Co-authored-by: Sasha Sobran <asobran@google.com> PiperOrigin-RevId: 889912141
1 parent 7aa1f52 commit 96e845e

File tree

2 files changed

+43
-3
lines changed

2 files changed

+43
-3
lines changed

src/google/adk/cli/fast_api.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ def _normalize_relative_path(path: str) -> str:
291291
def _has_parent_reference(path: str) -> bool:
292292
return any(part == ".." for part in path.split("/"))
293293

294-
_ALLOWED_UPLOAD_EXTENSIONS = frozenset({".yaml", ".yml"})
294+
_ALLOWED_EXTENSIONS = frozenset({".yaml", ".yml"})
295295

296296
def _parse_upload_filename(filename: Optional[str]) -> tuple[str, str]:
297297
if not filename:
@@ -307,10 +307,10 @@ def _parse_upload_filename(filename: Optional[str]) -> tuple[str, str]:
307307
if _has_parent_reference(rel_path):
308308
raise ValueError(f"Path traversal rejected: {filename!r}")
309309
ext = os.path.splitext(rel_path)[1].lower()
310-
if ext not in _ALLOWED_UPLOAD_EXTENSIONS:
310+
if ext not in _ALLOWED_EXTENSIONS:
311311
raise ValueError(
312312
f"File type not allowed: {rel_path!r}"
313-
f" (allowed: {', '.join(sorted(_ALLOWED_UPLOAD_EXTENSIONS))})"
313+
f" (allowed: {', '.join(sorted(_ALLOWED_EXTENSIONS))})"
314314
)
315315
return app_name, rel_path
316316

@@ -322,6 +322,12 @@ def _parse_file_path(file_path: str) -> str:
322322
raise ValueError(f"Absolute file_path rejected: {file_path!r}")
323323
if _has_parent_reference(file_path):
324324
raise ValueError(f"Path traversal rejected: {file_path!r}")
325+
ext = os.path.splitext(file_path)[1].lower()
326+
if ext not in _ALLOWED_EXTENSIONS:
327+
raise ValueError(
328+
f"File type not allowed: {file_path!r}"
329+
f" (allowed: {', '.join(sorted(_ALLOWED_EXTENSIONS))})"
330+
)
325331
return file_path
326332

327333
def _resolve_under_dir(root_dir: Path, rel_path: str) -> Path:

tests/unittests/cli/test_fast_api.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1759,6 +1759,40 @@ def test_builder_save_allows_yaml_files(builder_test_client, tmp_path):
17591759
assert response.json() is True
17601760

17611761

1762+
def test_builder_get_rejects_non_yaml_file_paths(builder_test_client, tmp_path):
1763+
"""GET /builder/app/{app_name}?file_path=... rejects non-YAML extensions."""
1764+
app_root = tmp_path / "app"
1765+
app_root.mkdir(parents=True, exist_ok=True)
1766+
(app_root / ".env").write_text("SECRET=supersecret\n")
1767+
(app_root / "agent.py").write_text("root_agent = None\n")
1768+
(app_root / "config.json").write_text("{}\n")
1769+
1770+
for file_path in [".env", "agent.py", "config.json"]:
1771+
response = builder_test_client.get(
1772+
f"/builder/app/app?file_path={file_path}"
1773+
)
1774+
assert response.status_code == 200, f"Expected 200 for {file_path}"
1775+
assert response.text == "", f"Expected empty response for {file_path}"
1776+
1777+
1778+
def test_builder_get_allows_yaml_file_paths(builder_test_client, tmp_path):
1779+
"""GET /builder/app/{app_name}?file_path=... allows YAML extensions."""
1780+
app_root = tmp_path / "app"
1781+
app_root.mkdir(parents=True, exist_ok=True)
1782+
(app_root / "sub_agent.yaml").write_text("name: sub\n")
1783+
(app_root / "tool.yml").write_text("name: tool\n")
1784+
1785+
response = builder_test_client.get(
1786+
"/builder/app/app?file_path=sub_agent.yaml"
1787+
)
1788+
assert response.status_code == 200
1789+
assert response.text == "name: sub\n"
1790+
1791+
response = builder_test_client.get("/builder/app/app?file_path=tool.yml")
1792+
assert response.status_code == 200
1793+
assert response.text == "name: tool\n"
1794+
1795+
17621796
def test_builder_endpoints_not_registered_without_web(
17631797
mock_session_service,
17641798
mock_artifact_service,

0 commit comments

Comments
 (0)