Skip to content

Commit ffda17b

Browse files
authored
fix(files): bind upload completion to signed path (#227)
1 parent 7e8599d commit ffda17b

2 files changed

Lines changed: 32 additions & 0 deletions

File tree

‎src/app/api/files/upload-complete/route.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ export async function POST(request, { params } = {}) {
7070
return NextResponse.json({ error: 'Invalid encrypted metadata' }, { status: 400 });
7171
}
7272

73+
// Only complete the object created by upload-url for this authenticated
74+
// user, conversation, and file id. Without this check a caller could
75+
// register an unrelated object path against an authorized message.
76+
const expectedStoragePath = `${user.id}/${conversationId}/${fileId}`;
77+
if (storagePath !== expectedStoragePath) {
78+
console.error('UPLOAD-COMPLETE: Storage path does not match upload request');
79+
return NextResponse.json({ error: 'Storage path does not match upload request' }, { status: 403 });
80+
}
81+
7382
// Get the internal user ID from the users table using auth_user_id
7483
const { data: internalUser, error: userError } = await supabase
7584
.from('users')

‎src/app/api/files/upload-complete/route.test.js‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,27 @@ describe('POST /api/files/upload-complete validation', () => {
131131
expect(mocks.from).not.toHaveBeenCalled();
132132
expect(mocks.broadcastToRoom).not.toHaveBeenCalled();
133133
});
134+
135+
it('rejects a storage path that is not bound to the authenticated upload', async () => {
136+
const { POST } = await import('./route.js');
137+
const request = {
138+
json: vi.fn().mockResolvedValue({
139+
storagePath: 'another-user/conversation-1/file-1',
140+
fileId: 'file-1',
141+
metadata: {
142+
messageId: 'message-1',
143+
conversationId: 'conversation-1',
144+
encryptedMetadata: { 'user-1': 'encrypted-metadata' }
145+
}
146+
})
147+
};
148+
149+
const response = await POST(request);
150+
const body = await response.json();
151+
152+
expect(response.status).toBe(403);
153+
expect(body.error).toBe('Storage path does not match upload request');
154+
expect(mocks.from).not.toHaveBeenCalled();
155+
expect(mocks.broadcastToRoom).not.toHaveBeenCalled();
156+
});
134157
});

0 commit comments

Comments
 (0)