Repository navigation
Give proxies a supported way to know which request fields are client-controlled (and reject them) in the built-in API server #7575
Description
🔴 Required Information
Is your feature request related to a specific problem?
The ApiServer docstring says the served endpoints are unauthenticated and must
be put behind the user's own authentication layer. That is what we did: a
gateway authenticates the caller and then forwards to the ADK endpoints.
The gap is what that layer is supposed to do with the request body. Several
request fields are written by the client straight into the run or the session:
RunAgentRequest.state_delta→ applied to session state (api_server.py,
forwarded torun_async).RunAgentRequest.service_tier→ forwarded to the model calls.CreateSessionRequest.state,.events,.options→ session initialization.
In an application that derives trusted values (user identity, groups, an
approval token) from session state, a client that can write state_delta can
forge them, so the gateway has to refuse these fields. There is no supported way
to do that, and a hand-written denylist is fragile for two reasons:
- Two spellings per field. The request models inherit
alias_generator=to_camelandpopulate_by_name=True
(cli/utils/common.py), sostate_deltaandstateDeltaare both accepted.
Filtering only the snake_case or only the camelCase name is a bypass. This is
not mentioned where the docstring recommends adding an auth layer. - The set of fields grows every minor.
service_tierand
CreateSessionRequest.optionswere added in recent releases, both
client-controlled. A denylist written against an earlier version silently
stops covering the request model after an upgrade.
Our workaround is to derive the field names from the pydantic models at import
time and to fail a unit test whenever a new field appears in
RunAgentRequest / CreateSessionRequest that we have not classified. That test
failed on both upgrades above, which is how we noticed, but it is code every
deployment behind a proxy has to rediscover and maintain.
Describe the Solution You'd Like
In increasing order of effort, whichever the maintainers prefer:
- Docs: in the
ApiServersecurity note, state that request models accept
both snake_case and camelCase names, and list the fields that write into
session state or run configuration. - A stable, exported source of truth: e.g. a module-level constant (or a
classmethod on the request models) naming the client-controlled fields, kept
next to the models so a new field has to be classified when it is added. - Opt-out in the server: an option on
get_fast_api_app(for example
trust_client_state=False) that rejectsstate_delta,CreateSessionRequest.state
/eventswith a 4xx, for deployments where state must come only from the
server side.
Impact on your work
We run ADK behind an authenticating gateway and need to guarantee that trusted
session state can only be written server-side. Today that guarantee depends on a
private denylist plus a test that must be re-checked on every ADK upgrade.
Willingness to contribute
Yes. Option 1 and 2 are small and I can open a PR with unit tests once the
preferred shape is agreed. Option 3 needs a design decision first.
🟡 Recommended Information
Describe Alternatives You've Considered
- Hand-maintained denylist of field names: bypassable via the alias spelling and
goes stale when fields are added. - Deriving the names from the models (what we do): robust, but relies on the
models' internals and on re-running a test after each upgrade. - Sub-classing the server and overriding request handling: rejected, it forks
handler logic that changes between releases.
Proposed API / Implementation
# google/adk/cli/api_server.py (illustrative)
CLIENT_CONTROLLED_RUN_FIELDS: frozenset[str] = frozenset(
{"state_delta", "service_tier"}
)
CLIENT_CONTROLLED_SESSION_FIELDS: frozenset[str] = frozenset(
{"state", "events", "options"}
)
app = get_fast_api_app(..., trust_client_state=False) # option 3Additional Context
This is not a vulnerability report: the documented model is "unauthenticated,
put your own layer in front". The request is for that layer to have a supported
and complete description of what the body can carry.
Activity
sattyamjjain commentedon Oct 17, 2026
Two more state-write paths that a field list built from RunAgentRequest and CreateSessionRequest would miss (checked at 0e96818, api_server.py):
state_delta: dict[str, Any]and appends it as a user event without running the agent (L1795-1835).stateas a bare parameter, not through a request model.Also, RunAgentRequest carries
user_idin the body (L654), so identity is client-supplied too unless the proxy pins it. Other client-controlled fields there: function_call_event_id, invocation_id, and custom_metadata (L660-663).Untested idea for the proxy: parse the body with ADK's own models and check
model_fields_setagainst an allowlist. The models use camelCase aliases with populate_by_name=True, and model_fields_set holds the field names, whichever spelling was sent.https://github.com/google/adk-python/blob/0e968189596f93a0220f8f7bc63ebe3a569765a8/src/google/adk/cli/api_server.py
Oliveryfaso commentedon Oct 21, 2026
Would a documentation + transport-contract test slice be useful here, coordinated with @Javviviii2's proposed implementation? I checked the unmodified checkout at
77a74f0before proposing a public helper or server option.Building on @sattyamjjain's PATCH/legacy-route findings, there are two additional boundaries to account for:
/run_live: each text frame is parsed asLiveRequest, which has a top-levelstate_delta. Unlike the three HTTP request models, this model does not use the camelCase alias generator:state_deltareaches the live queue; top-levelstateDeltais currently ignored. HTTP OpenAPI includes neither this route nor its message model. See frame parsing and LiveRequest. This is a contract observation, not a vulnerability claim.ADK_DEFAULT_APP_NAME,/users/...is rewritten to/apps/{default_app}/users/...before routing. A deployment's path policy must account for these forms too; they are not separate OpenAPI paths. See rewrite middleware.The state/run input inventory is:
POST /run,/run_sseapp_name,user_id,session_idin bodynew_message,streaming,state_delta,function_call_event_id,invocation_id,custom_metadata,service_tierPOST /apps/{app_name}/users/{user_id}/sessionssession_idin bodystate,events,optionsPATCH .../sessions/{session_id}state_deltaPOST .../sessions/{session_id}{"state": ...}envelope/run_liveLiveRequest, includingstate_delta; query also carries live-run configurationThese inputs have different effects. For example,
function_call_event_idis not forwarded as a runner argument, but affects SSE artifact-event grouping;streamingselects streaming mode on/run_sse, while/runremains nonstreaming. Session initialization events also have existing runtime-protocol validation. A field list should describe these contracts without declaring fields universally safe for every application's authorization policy.The
model_fields_setidea does normalize recognized HTTP field names, including explicitnull,{}, andfalse. Two caveats were reproducible:model_fields_set. Checking that set and then forwarding the original JSON would lose this information. Per-callmodel_validate(payload, extra="forbid")rejects unknown keys without changing ADK's model defaults in the Pydantic version tested.Minimal model-only reproduction in that checkout's environment:
Validation performed: a local characterization suite passed 70 tests on macOS arm64 / Python 3.12.13 / Pydantic 2.14.1. It uses TestClient, a recording fake runner, and an in-memory session service; outbound socket connections are blocked during the tests. It covers both HTTP spellings, presence versus null/empty/false, unknown and duplicate keys, both run routes, create/PATCH/legacy paths, live-frame parsing into the queue, and default-app shorthand. An inventory exported 22 production HTTP operations plus one WebSocket route. No model/provider E2E, full upstream tox matrix, or deployed gateway was tested; the suite is local research, not upstream CI or a complete authorization gateway.
Proposed first slice: document the HTTP aliases, client-writable state/run inputs, bare legacy body, and explicit WebSocket scope; add focused transport tests in the existing API-server test module. A separate supported descriptor/helper or opt-out should follow whichever design maintainers prefer. This would preserve current endpoint behavior and stay separate from the caller-principal work in #7572/#7134 and authentication middleware in #5981.
Is that first slice welcome, and is it already covered by the reporter or an internal change? Happy to coordinate tests/docs with the reporter instead of duplicating their implementation.
AI assistance disclosure: Codex prepared this analysis and executed the local characterization tests. No claim of manual provider testing by the account owner.