|
47 | 47 | _COMPACTION_CUSTOM_METADATA_KEY = '_compaction' |
48 | 48 | _USAGE_METADATA_CUSTOM_METADATA_KEY = '_usage_metadata' |
49 | 49 |
|
| 50 | +_SESSION_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]+$') |
| 51 | + |
| 52 | + |
| 53 | +def _validate_session_id(session_id: str) -> None: |
| 54 | + """Rejects session IDs that could escape the URL path segment.""" |
| 55 | + if not isinstance(session_id, str) or not _SESSION_ID_PATTERN.fullmatch( |
| 56 | + session_id |
| 57 | + ): |
| 58 | + raise ValueError( |
| 59 | + f'Invalid session_id {session_id!r}: must match' |
| 60 | + f' {_SESSION_ID_PATTERN.pattern}.' |
| 61 | + ) |
| 62 | + |
50 | 63 |
|
51 | 64 | def _quote_filter_literal(value: str) -> str: |
52 | 65 | """Quotes filter values so embedded metacharacters stay inside the literal.""" |
@@ -134,6 +147,7 @@ async def create_session( |
134 | 147 |
|
135 | 148 | config = {'session_state': state} if state else {} |
136 | 149 | if session_id: |
| 150 | + _validate_session_id(session_id) |
137 | 151 | config['session_id'] = session_id |
138 | 152 | config.update(kwargs) |
139 | 153 | async with self._get_api_client() as api_client: |
@@ -164,6 +178,7 @@ async def get_session( |
164 | 178 | session_id: str, |
165 | 179 | config: Optional[GetSessionConfig] = None, |
166 | 180 | ) -> Optional[Session]: |
| 181 | + _validate_session_id(session_id) |
167 | 182 | reasoning_engine_id = self._get_reasoning_engine_id(app_name) |
168 | 183 | session_resource_name = ( |
169 | 184 | f'reasoningEngines/{reasoning_engine_id}/sessions/{session_id}' |
@@ -263,14 +278,30 @@ async def list_sessions( |
263 | 278 | async def delete_session( |
264 | 279 | self, *, app_name: str, user_id: str, session_id: str |
265 | 280 | ) -> None: |
| 281 | + _validate_session_id(session_id) |
266 | 282 | reasoning_engine_id = self._get_reasoning_engine_id(app_name) |
| 283 | + session_resource_name = ( |
| 284 | + f'reasoningEngines/{reasoning_engine_id}/sessions/{session_id}' |
| 285 | + ) |
267 | 286 |
|
268 | 287 | async with self._get_api_client() as api_client: |
| 288 | + # Enforce ownership: delete_session otherwise ignores user_id entirely. |
| 289 | + try: |
| 290 | + existing = await api_client.agent_engines.sessions.get( |
| 291 | + name=session_resource_name |
| 292 | + ) |
| 293 | + except ClientError as e: |
| 294 | + if e.code == 404: |
| 295 | + return |
| 296 | + raise |
| 297 | + if existing.user_id != user_id: |
| 298 | + raise ValueError( |
| 299 | + f'Session {session_id} does not belong to user {user_id}.' |
| 300 | + ) |
| 301 | + |
269 | 302 | try: |
270 | 303 | await api_client.agent_engines.sessions.delete( |
271 | | - name=( |
272 | | - f'reasoningEngines/{reasoning_engine_id}/sessions/{session_id}' |
273 | | - ), |
| 304 | + name=session_resource_name, |
274 | 305 | ) |
275 | 306 | except Exception as e: |
276 | 307 | logger.error('Error deleting session %s: %s', session_id, e) |
@@ -302,6 +333,7 @@ async def append_event(self, session: Session, event: Event) -> Event: |
302 | 333 | # Update the in-memory session. |
303 | 334 | await super().append_event(session=session, event=event) |
304 | 335 |
|
| 336 | + _validate_session_id(session.id) |
305 | 337 | reasoning_engine_id = self._get_reasoning_engine_id(session.app_name) |
306 | 338 |
|
307 | 339 | # Build config (Monolithic approach) |
|
0 commit comments