diff --git a/src/app/api/files/upload-complete/route.js b/src/app/api/files/upload-complete/route.js index 7dc940b..0f41110 100644 --- a/src/app/api/files/upload-complete/route.js +++ b/src/app/api/files/upload-complete/route.js @@ -70,6 +70,15 @@ export async function POST(request, { params } = {}) { return NextResponse.json({ error: 'Invalid encrypted metadata' }, { status: 400 }); } + // Only complete the object created by upload-url for this authenticated + // user, conversation, and file id. Without this check a caller could + // register an unrelated object path against an authorized message. + const expectedStoragePath = `${user.id}/${conversationId}/${fileId}`; + if (storagePath !== expectedStoragePath) { + console.error('UPLOAD-COMPLETE: Storage path does not match upload request'); + return NextResponse.json({ error: 'Storage path does not match upload request' }, { status: 403 }); + } + // Get the internal user ID from the users table using auth_user_id const { data: internalUser, error: userError } = await supabase .from('users') diff --git a/src/app/api/files/upload-complete/route.test.js b/src/app/api/files/upload-complete/route.test.js index e545f12..228c548 100644 --- a/src/app/api/files/upload-complete/route.test.js +++ b/src/app/api/files/upload-complete/route.test.js @@ -131,4 +131,27 @@ describe('POST /api/files/upload-complete validation', () => { expect(mocks.from).not.toHaveBeenCalled(); expect(mocks.broadcastToRoom).not.toHaveBeenCalled(); }); + + it('rejects a storage path that is not bound to the authenticated upload', async () => { + const { POST } = await import('./route.js'); + const request = { + json: vi.fn().mockResolvedValue({ + storagePath: 'another-user/conversation-1/file-1', + fileId: 'file-1', + metadata: { + messageId: 'message-1', + conversationId: 'conversation-1', + encryptedMetadata: { 'user-1': 'encrypted-metadata' } + } + }) + }; + + const response = await POST(request); + const body = await response.json(); + + expect(response.status).toBe(403); + expect(body.error).toBe('Storage path does not match upload request'); + expect(mocks.from).not.toHaveBeenCalled(); + expect(mocks.broadcastToRoom).not.toHaveBeenCalled(); + }); });