🔴 Required Information
Describe the Bug:
artifact_util.validate_path_segment no longer rejects path separators inside a value, only a leading one. FileArtifactService builds its user-scoped and session-scoped directories so that they differ by an infix, so a user_id containing /sessions/<id> now resolves to the same directory as another user's session scope. In practice that means artifacts saved under one caller's user scope can land on, and read back, another caller's session-scoped artifacts.
This looks like an unintended regression from 45a77dc5 ("fix: Validate path segments in GcsArtifactService and InMemoryArtifactService to prevent cross-user artifact access"). That commit consolidated the validator into artifact_util, and in the process replaced this check:
if "/" in value or "\\" in value:
raise InputValidationError(
f"{field_name} {value!r} must not contain path separators."
)
with one that only catches a leading separator (artifact_util.py:165-171 at v2.8.0):
if isinstance(value, str) and (
value.startswith("/") or value.startswith("\\")
):
raise input_validation_error.InputValidationError(
f"{field_name} {value!r} must not be an absolute path or start with a"
" slash."
)
.., null bytes and drive-qualified values are still rejected; an interior / is not.
Worth noting that issue #6115 cited FileArtifactService as the safe reference implementation at the time, precisely because it "rejects path separators". GcsArtifactService and InMemoryArtifactService are not affected by this, because they build {app}/{user}/user/{file} rather than .../sessions/{id}/artifacts/{file}.
Steps to Reproduce:
- Create a
FileArtifactService on a temporary root.
- Save a session-scoped artifact for
user_id="victim", session_id="s1", filename="notes.txt".
- Save a user-scoped artifact for
user_id="victim/sessions/s1", filename="user:notes.txt" (the user: prefix selects user scope; the value contains no .. and no leading slash, so it passes validation).
- Load the artifact from step 2 again.
Expected Behavior:
Step 3 raises InputValidationError, because user_id contains a path separator — the behaviour before 45a77dc5.
Observed Behavior:
Step 3 is accepted, and both scopes resolve to the same directory:
<root>/apps/app/users/victim/sessions/s1/artifacts/notes.txt
so step 4 returns the value written in step 3.
Environment Details:
- ADK version: 2.8.0 (also present from 2.5.0 onward — every release containing
45a77dc5)
- Python version: 3.10.1
- OS: Windows 11; the path handling here is not platform-specific
- Artifact service:
FileArtifactService
Model Information:
Not applicable — no model or API key is involved.
🟡 Optional Information
Regression:
Yes. Introduced in 45a77dc5 (2026-07-13), first released in v2.5.0. The removed check dates back to cbcb5e60.
Minimal Reproduction Code:
import tempfile
from pathlib import Path
from google.adk.artifacts.file_artifact_service import FileArtifactService
svc = FileArtifactService(root_dir=Path(tempfile.mkdtemp()))
print(svc._artifact_dir("app", "victim", "s1", "notes.txt"))
print(svc._artifact_dir("app", "victim/sessions/s1", None, "user:notes.txt"))
Both lines print the same path.
Additional Context:
Suggested fix is to restore the separator check in validate_path_segment:
if "/" in value or "\\" in value:
raise input_validation_error.InputValidationError(
f"{field_name} {value!r} must not contain path separators."
)
This subsumes the existing startswith branch. A regression test asserting user_id="a/b" raises would be worth adding, since this property has now moved twice.
I looked at how far this reaches through the shipped entry points. HTTP path parameters are not a route in: uvicorn percent-decodes before routing, so %2F in {user_id} changes which route matches. The paths where a separator can reach the artifact layer are POST /run when --auto_create_session is set (userId arrives in the JSON body, so no URL decoding applies) and the A2A server, where converters/request_converter.py:67 derives user_id from the client-supplied context_id. _make_trigger_user_id already guards against this by replacing / with --, which suggests a separator in user_id is understood elsewhere in the codebase to be unsafe.
I'm filing this as a bug rather than through a security channel, since the API server is documented as unauthenticated and I couldn't demonstrate a deployment where this crosses a privilege boundary. Happy to share the fuller reproduction privately if you'd prefer.
🔴 Required Information
Describe the Bug:
artifact_util.validate_path_segmentno longer rejects path separators inside a value, only a leading one.FileArtifactServicebuilds its user-scoped and session-scoped directories so that they differ by an infix, so auser_idcontaining/sessions/<id>now resolves to the same directory as another user's session scope. In practice that means artifacts saved under one caller's user scope can land on, and read back, another caller's session-scoped artifacts.This looks like an unintended regression from
45a77dc5("fix: Validate path segments in GcsArtifactService and InMemoryArtifactService to prevent cross-user artifact access"). That commit consolidated the validator intoartifact_util, and in the process replaced this check:with one that only catches a leading separator (
artifact_util.py:165-171at v2.8.0):.., null bytes and drive-qualified values are still rejected; an interior/is not.Worth noting that issue #6115 cited
FileArtifactServiceas the safe reference implementation at the time, precisely because it "rejects path separators".GcsArtifactServiceandInMemoryArtifactServiceare not affected by this, because they build{app}/{user}/user/{file}rather than.../sessions/{id}/artifacts/{file}.Steps to Reproduce:
FileArtifactServiceon a temporary root.user_id="victim",session_id="s1",filename="notes.txt".user_id="victim/sessions/s1",filename="user:notes.txt"(theuser:prefix selects user scope; the value contains no..and no leading slash, so it passes validation).Expected Behavior:
Step 3 raises
InputValidationError, becauseuser_idcontains a path separator — the behaviour before45a77dc5.Observed Behavior:
Step 3 is accepted, and both scopes resolve to the same directory:
so step 4 returns the value written in step 3.
Environment Details:
45a77dc5)FileArtifactServiceModel Information:
Not applicable — no model or API key is involved.
🟡 Optional Information
Regression:
Yes. Introduced in
45a77dc5(2026-07-13), first released in v2.5.0. The removed check dates back tocbcb5e60.Minimal Reproduction Code:
Both lines print the same path.
Additional Context:
Suggested fix is to restore the separator check in
validate_path_segment:This subsumes the existing
startswithbranch. A regression test assertinguser_id="a/b"raises would be worth adding, since this property has now moved twice.I looked at how far this reaches through the shipped entry points. HTTP path parameters are not a route in: uvicorn percent-decodes before routing, so
%2Fin{user_id}changes which route matches. The paths where a separator can reach the artifact layer arePOST /runwhen--auto_create_sessionis set (userIdarrives in the JSON body, so no URL decoding applies) and the A2A server, whereconverters/request_converter.py:67derivesuser_idfrom the client-suppliedcontext_id._make_trigger_user_idalready guards against this by replacing/with--, which suggests a separator inuser_idis understood elsewhere in the codebase to be unsafe.I'm filing this as a bug rather than through a security channel, since the API server is documented as unauthenticated and I couldn't demonstrate a deployment where this crosses a privilege boundary. Happy to share the fuller reproduction privately if you'd prefer.