Skip to content

fix(sessions/vertexai): quote userId in AIP-160 list filter#499

Merged
kalenkevich merged 2 commits into
google:mainfrom
petrmarinec:fix-vertex-session-filter-escaping
Jul 20, 2026
Merged

fix(sessions/vertexai): quote userId in AIP-160 list filter#499
kalenkevich merged 2 commits into
google:mainfrom
petrmarinec:fix-vertex-session-filter-escaping

Conversation

@petrmarinec

Copy link
Copy Markdown
Contributor

Link to Issue or Description of Change

Problem

VertexAiSessionService.listSessions in core/src/sessions/vertex_ai_session_service.ts builds the Vertex AI Agent Engine list filter by interpolating the raw userId directly into a quoted AIP-160 string literal:

...(userId ? {filter: `user_id="${userId}"`} : {}),

A double-quote character in userId breaks out of the quoted value and lets a caller append arbitrary AIP-160 predicates. For example a userId of attacker" OR user_id!=" produces the filter:

user_id="attacker" OR user_id!=""

Since user_id!="" matches every session with a non-empty user id, this returns sessions belonging to all users of the same Agent Engine deployment (cross-user session enumeration). Each returned session exposes its id, user id, update time, and state.

This is reachable from the dev API server: the route GET /apps/:appName/users/:userId/sessions (dev/src/server/adk_api_server.ts) reads req.params.userId and passes it straight to sessionService.listSessions({appName, userId}), so the attacker only needs to URL-encode " in the path.

This is the JS/TS counterpart of an issue previously reported and fixed in adk-python — see google/adk-python#5270 / PR google/adk-python#5273 (merged as bdece00), where the same unescaped-user_id interpolation was fixed with a filter-literal quoting helper. I noticed the same pattern here while reviewing the sibling port and am proposing the equivalent fix.

Solution

Add a small quoteFilterLiteral helper that escapes backslashes first, then double-quotes, so the whole value stays inside the AIP-160 string literal regardless of what is passed in, and use it when building the filter:

...(userId ? {filter: `user_id=${quoteFilterLiteral(userId)}`} : {}),

Testing Plan

Unit tests

Added a regression test to the existing listSessions suite in core/test/sessions/vertex_ai_session_service_test.ts that passes a quote-injection userId and asserts the exact (escaped) filter forwarded to the client:

userId: 'attacker" OR user_id!="'
// asserts: config: {filter: 'user_id="attacker\\" OR user_id!=\\""'}

The pre-existing listSessions test (userId: 'testUser' -> filter: 'user_id="testUser"') continues to hold, since the helper is a no-op for values without metacharacters.

Manual validation (filter-string transformation)

  • On main, a userId of attacker" OR user_id!=" produces the filter user_id="attacker" OR user_id!="" — the injected OR user_id!="" predicate is live and returns every user's sessions.
  • With this change, the same input produces user_id="attacker\" OR user_id!=\"" — the metacharacters stay inside the quoted literal and the filter matches only the literal (non-existent) user.

Additional context

Small, focused fix that keeps the current filter-construction approach but ensures userId remains data instead of altering the filter expression. Mirrors the escaping order used in the merged adk-python fix (backslashes first, then double quotes).

VertexAiSessionService.listSessions built the Vertex AI Agent Engine list
filter by interpolating the raw userId into a quoted AIP-160 string literal
(`user_id="${userId}"`). A double quote in userId breaks out of the literal and
appends attacker-controlled predicates, e.g. userId `attacker" OR user_id!="`
yields the filter `user_id="attacker" OR user_id!=""`, which returns sessions
for every user of the deployment (cross-user session enumeration). This service
is reachable from the dev API server route GET
/apps/:appName/users/:userId/sessions, where userId is a caller-controlled path
parameter.

Add quoteFilterLiteral, which escapes backslashes then double quotes so the
value stays inside the literal, and use it when building the filter. Adds a
regression test asserting the escaped filter for a quote-injection payload.

This is the JS/TS counterpart of the same issue previously fixed in adk-python
(google/adk-python#5270, PR google/adk-python#5273, commit bdece00).
Add direct unit tests for quoteFilterLiteral covering plain value, quote
injection, backslash-only, and empty input, matching the edge-case coverage of
the adk-python fix (google/adk-python#5273).
@Varun-S10 Varun-S10 self-assigned this Jul 20, 2026
@Varun-S10

Copy link
Copy Markdown
Contributor

Hi @kalenkevich, I have checked the issue and validated the proposed changes. I was able to reproduce the issue. Could you please review this PR?

@Varun-S10 Varun-S10 added the needs review [Status] The PR/issue is awaiting review from the maintainer label Jul 20, 2026
@kalenkevich
kalenkevich merged commit 8f9e8f2 into google:main Jul 20, 2026
12 checks passed
This was referenced Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs review [Status] The PR/issue is awaiting review from the maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants