From 01cd022f2067fb8c3eff3184056f760d8f26c671 Mon Sep 17 00:00:00 2001 From: prasanna8585 Date: Mon, 17 Aug 2026 09:52:57 +0530 Subject: [PATCH] Enforce session ownership in VertexAiSessionService.listEvents Commit d1b1d92 added an ownership check to getSession and deleteSession (compare the backend-reported session owner to the caller's userId, deny as not-found on mismatch), but the same check was not applied to listEvents. listEvents returns a session's full event stream (conversation content, tool calls, state deltas) addressed solely by sessionId. The Vertex backend session-events sub-resource does not enforce per-caller ownership on its own (per the existing comments on the getSession/ deleteSession checks), so without a client-side check any authenticated caller who knows or guesses another user's sessionId can read that user's full conversation history via listEvents, the same disclosure class d1b1d92 fixed for getSession/deleteSession, left open here. Fixes listEvents to fetch the session and apply the same ownership check getSession/deleteSession already use before returning events, denying as empty (not an error) on mismatch, consistent with getSession's not-found-style denial. Adds a regression test (listEvents_wrongUser_returnsEmpty) mirroring the existing getSession_wrongUser_returnsEmpty test. --- .../adk/sessions/VertexAiSessionService.java | 21 ++++++++++++++++++- .../sessions/VertexAiSessionServiceTest.java | 11 ++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java b/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java index 92c10cd97..1bf00d998 100644 --- a/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java +++ b/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java @@ -165,7 +165,26 @@ private ListSessionsResponse parseListSessionsResponse( @Override public Single listEvents(String appName, String userId, String sessionId) { validateSessionId(sessionId); - return listEventsInternal(appName, sessionId, /* filter= */ null); + String reasoningEngineId = parseReasoningEngineId(appName); + return client + .getSession(reasoningEngineId, sessionId) + .flatMapSingle( + getSessionResponseMap -> { + // Enforce ownership using the owner reported by the backend, not the + // requested user id, mirroring the check getSession/deleteSession use. + // Deny as empty so existence is not revealed to a non-owner. + String ownerUserId = + Optional.ofNullable(getSessionResponseMap.get("userId")) + .map(JsonNode::asText) + .orElse(null); + if (!userId.equals(ownerUserId)) { + return Single.just(ListEventsResponse.builder().build()); + } + return listEventsInternal(appName, sessionId, /* filter= */ null); + }) + // No session found at all (rather than an ownership mismatch): behave the + // same as before, returning an empty response instead of erroring. + .defaultIfEmpty(ListEventsResponse.builder().build()); } private Single listEventsInternal( diff --git a/core/src/test/java/com/google/adk/sessions/VertexAiSessionServiceTest.java b/core/src/test/java/com/google/adk/sessions/VertexAiSessionServiceTest.java index db3556956..26569e3ec 100644 --- a/core/src/test/java/com/google/adk/sessions/VertexAiSessionServiceTest.java +++ b/core/src/test/java/com/google/adk/sessions/VertexAiSessionServiceTest.java @@ -422,6 +422,17 @@ public void getSession_wrongUser_returnsEmpty() { .isNull(); } + @Test + public void listEvents_wrongUser_returnsEmpty() { + // Session "1" belongs to "user" and has an event in eventMap (MOCK_EVENT_STRING). + // A different user must not be able to read those events - mirrors + // getSession_wrongUser_returnsEmpty above, applied to listEvents. + ListEventsResponse response = + vertexAiSessionService.listEvents("123", "attacker", "1").blockingGet(); + + assertThat(response.events()).isEmpty(); + } + @Test public void deleteSession_wrongUser_deniedAndSessionKept() { // The ownership error surfaces on subscription, so hoist the Completable out.