diff --git a/packages/a2a-server/src/persistence/gcs.test.ts b/packages/a2a-server/src/persistence/gcs.test.ts index 83322df5ab..172b6e0b90 100644 --- a/packages/a2a-server/src/persistence/gcs.test.ts +++ b/packages/a2a-server/src/persistence/gcs.test.ts @@ -227,6 +227,35 @@ describe('GCSTaskStore', () => { 'tar.c command failed to create', ); }); + + it('should throw an error if taskId contains path traversal sequences', async () => { + const store = new GCSTaskStore('test-bucket'); + const maliciousTask: SDKTask = { + id: '../../../malicious-task', + metadata: { + _internal: { + agentSettings: { + cacheDir: '/tmp/cache', + dataDir: '/tmp/data', + logDir: '/tmp/logs', + tempDir: '/tmp/temp', + }, + taskState: 'working', + }, + }, + kind: 'task', + status: { + state: 'working', + timestamp: new Date().toISOString(), + }, + contextId: 'test-context', + history: [], + artifacts: [], + }; + await expect(store.save(maliciousTask)).rejects.toThrow( + 'Invalid taskId: ../../../malicious-task', + ); + }); }); describe('load', () => { @@ -323,6 +352,14 @@ describe('GCSTaskStore', () => { ); }); }); + + it('should throw an error if taskId contains path traversal sequences', async () => { + const store = new GCSTaskStore('test-bucket'); + const maliciousTaskId = '../../../malicious-task'; + await expect(store.load(maliciousTaskId)).rejects.toThrow( + `Invalid taskId: ${maliciousTaskId}`, + ); + }); }); describe('NoOpTaskStore', () => { diff --git a/packages/a2a-server/src/persistence/gcs.ts b/packages/a2a-server/src/persistence/gcs.ts index ef952f49ab..d42ae02270 100644 --- a/packages/a2a-server/src/persistence/gcs.ts +++ b/packages/a2a-server/src/persistence/gcs.ts @@ -23,6 +23,13 @@ type ObjectType = 'metadata' | 'workspace'; const getTmpArchiveFilename = (taskId: string): string => `task-${taskId}-workspace-${uuidv4()}.tar.gz`; +// Validate the taskId to prevent path traversal attacks by ensuring it only contains safe characters. +const isTaskIdValid = (taskId: string): boolean => { + // Allow only alphanumeric characters, dashes, and underscores, and ensure it's not empty. + const validTaskIdRegex = /^[a-zA-Z0-9_-]+$/; + return validTaskIdRegex.test(taskId); +}; + export class GCSTaskStore implements TaskStore { private storage: Storage; private bucketName: string; @@ -78,6 +85,9 @@ export class GCSTaskStore implements TaskStore { } private getObjectPath(taskId: string, type: ObjectType): string { + if (!isTaskIdValid(taskId)) { + throw new Error(`Invalid taskId: ${taskId}`); + } return `tasks/${taskId}/${type}.tar.gz`; }