From f2030343d7a3701d096622efaa22fc4aaadd174c Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Fri, 13 Mar 2026 15:22:07 -0700 Subject: [PATCH] Fix potential IDOR vulnerability in workspace parsed files endpoints Add ownership validation to prevent users from deleting or embedding parsed files that don't belong to them. Previously, the delete and embed endpoints only validated authentication but not resource ownership, allowing users to delete attached files for users within workspaces they are also a member of. Changes: - Delete endpoint now filters by userId and workspaceId - Embed endpoint validates file belongs to user and workspace (redundant) - delete() returns false when no matching records found (returns 403) - Added JSDoc comments for clarity GHSA-p5rf-8p88-979c --- server/endpoints/workspacesParsedFiles.js | 16 ++++++++++++--- server/models/workspaceParsedFiles.js | 24 +++++++++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/server/endpoints/workspacesParsedFiles.js b/server/endpoints/workspacesParsedFiles.js index 1bf0b97a..44b4a004 100644 --- a/server/endpoints/workspacesParsedFiles.js +++ b/server/endpoints/workspacesParsedFiles.js @@ -50,10 +50,16 @@ function workspaceParsedFilesEndpoints(app) { try { const { fileIds = [] } = reqBody(request); if (!fileIds.length) return response.sendStatus(400).end(); + const user = await userFromSession(request, response); + const workspace = response.locals.workspace; const success = await WorkspaceParsedFiles.delete({ - id: { in: fileIds.map((id) => parseInt(id)) }, + id: { + in: fileIds.map((id) => parseInt(id)), + }, + ...(user ? { userId: user.id } : {}), + workspaceId: workspace.id, }); - return response.status(success ? 200 : 500).end(); + return response.status(success ? 200 : 403).end(); } catch (e) { console.error(e.message, e); return response.sendStatus(500).end(); @@ -77,7 +83,11 @@ function workspaceParsedFilesEndpoints(app) { if (!fileId) return response.sendStatus(400).end(); const { success, error, document } = - await WorkspaceParsedFiles.moveToDocumentsAndEmbed(fileId, workspace); + await WorkspaceParsedFiles.moveToDocumentsAndEmbed( + user, + fileId, + workspace + ); if (!success) { return response.status(500).json({ diff --git a/server/models/workspaceParsedFiles.js b/server/models/workspaceParsedFiles.js index 37a538b9..ba692fce 100644 --- a/server/models/workspaceParsedFiles.js +++ b/server/models/workspaceParsedFiles.js @@ -43,6 +43,11 @@ const WorkspaceParsedFiles = { } }, + /** + * Gets a parsed file by its ID or a clause. + * @param {object} clause - The clause to filter the parsed files. + * @returns {Promise} The parsed file. + */ get: async function (clause = {}) { try { const file = await prisma.workspace_parsed_files.findFirst({ @@ -77,10 +82,10 @@ const WorkspaceParsedFiles = { delete: async function (clause = {}) { try { - await prisma.workspace_parsed_files.deleteMany({ + const result = await prisma.workspace_parsed_files.deleteMany({ where: clause, }); - return true; + return result.count > 0; } catch (error) { console.error(error.message); return false; @@ -95,9 +100,20 @@ const WorkspaceParsedFiles = { return _sum.tokenCountEstimate || 0; }, - moveToDocumentsAndEmbed: async function (fileId, workspace) { + /** + * Moves a parsed file to the documents and embeds it. + * @param {import("@prisma/client").users | null} user - The user performing the operation. + * @param {number} fileId - The ID of the parsed file. + * @param {import("@prisma/client").workspaces} workspace - The workspace the file belongs to. + * @returns {Promise<{ success: boolean, error: string | null, document: import("@prisma/client").workspace_documents | null }>} The result of the operation. + */ + moveToDocumentsAndEmbed: async function (user = null, fileId, workspace) { try { - const parsedFile = await this.get({ id: parseInt(fileId) }); + const parsedFile = await this.get({ + id: parseInt(fileId), + ...(user ? { userId: user.id } : {}), + workspaceId: workspace.id, + }); if (!parsedFile) throw new Error("File not found"); // Get file location from metadata