From 6fb93b35270b225b64e816f6092a9ca4cce7320b Mon Sep 17 00:00:00 2001 From: Daniel R Farrell Date: Sat, 13 Dec 2025 22:57:04 -0800 Subject: [PATCH 1/2] Fix CVE-2025-63783: Add authorization checks to project mutation APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit addresses a critical Broken Object Level Authorization (BOLA) vulnerability where authenticated users could modify, delete, or manipulate tags on projects they don't own by sending requests with arbitrary project IDs. Changes: - Add verifyProjectAccess() helper function to verify user project membership - Add authorization checks to delete, update, addTag, and removeTag mutations - Ensure all project mutations verify ownership before performing operations The fix validates that the authenticated user has access to the project via the userProjects junction table before allowing any mutation operations. Security Impact: Prevents unauthorized users from modifying or deleting projects that don't belong to them. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/server/api/routers/project/helper.ts | 30 ++++++++++++++++++- .../src/server/api/routers/project/project.ts | 6 +++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/apps/web/client/src/server/api/routers/project/helper.ts b/apps/web/client/src/server/api/routers/project/helper.ts index e2f6037378..2216ffaf47 100644 --- a/apps/web/client/src/server/api/routers/project/helper.ts +++ b/apps/web/client/src/server/api/routers/project/helper.ts @@ -1,4 +1,5 @@ -import { type Frame } from "@onlook/db"; +import { eq } from "drizzle-orm"; +import { type Frame, projects, userProjects, type DrizzleDb } from "@onlook/db"; export function extractCsbPort(frames: Frame[]): number | null { if (!frames || frames.length === 0) return null; @@ -17,3 +18,30 @@ export function extractCsbPort(frames: Frame[]): number | null { } return null; } + +/** + * Verifies that a user has access to a project by checking the userProjects table. + * @throws Error if the user does not have access to the project + */ +export async function verifyProjectAccess( + db: DrizzleDb, + userId: string, + projectId: string, +): Promise { + const project = await db.query.projects.findFirst({ + where: eq(projects.id, projectId), + with: { + userProjects: { + where: eq(userProjects.userId, userId), + }, + }, + }); + + if (!project) { + throw new Error('Project not found'); + } + + if (project.userProjects.length === 0) { + throw new Error('Unauthorized: You do not have access to this project'); + } +} diff --git a/apps/web/client/src/server/api/routers/project/project.ts b/apps/web/client/src/server/api/routers/project/project.ts index a110db5bbf..2747860a92 100644 --- a/apps/web/client/src/server/api/routers/project/project.ts +++ b/apps/web/client/src/server/api/routers/project/project.ts @@ -37,7 +37,7 @@ import { and, eq, ne } from 'drizzle-orm'; import { z } from 'zod'; import { projectCreateRequestRouter } from './createRequest'; import { fork } from './fork'; -import { extractCsbPort } from './helper'; +import { extractCsbPort, verifyProjectAccess } from './helper'; export const projectRouter = createTRPCRouter({ hasAccess: protectedProcedure @@ -348,6 +348,7 @@ export const projectRouter = createTRPCRouter({ delete: protectedProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) => { + await verifyProjectAccess(ctx.db, ctx.user.id, input.id); await ctx.db.transaction(async (tx) => { await tx.delete(projects).where(eq(projects.id, input.id)); await tx.delete(userProjects).where(eq(userProjects.projectId, input.id)); @@ -365,6 +366,7 @@ export const projectRouter = createTRPCRouter({ return projects.map((project) => fromDbProject(project.project)); }), update: protectedProcedure.input(projectUpdateSchema).mutation(async ({ ctx, input }) => { + await verifyProjectAccess(ctx.db, ctx.user.id, input.id); const [updatedProject] = await ctx.db.update(projects).set({ ...input, updatedAt: new Date(), @@ -380,6 +382,7 @@ export const projectRouter = createTRPCRouter({ projectId: z.string(), tag: z.string(), })).mutation(async ({ ctx, input }) => { + await verifyProjectAccess(ctx.db, ctx.user.id, input.projectId); const project = await ctx.db.query.projects.findFirst({ where: eq(projects.id, input.projectId), }); @@ -404,6 +407,7 @@ export const projectRouter = createTRPCRouter({ projectId: z.string(), tag: z.string(), })).mutation(async ({ ctx, input }) => { + await verifyProjectAccess(ctx.db, ctx.user.id, input.projectId); const project = await ctx.db.query.projects.findFirst({ where: eq(projects.id, input.projectId), }); From b713556ff7c3538afbe4c4d5f5a5a1c631884152 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 14 Dec 2025 07:05:11 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`fix?= =?UTF-8?q?-cve-2025-63783-idor`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @drfarrell. * https://github.com/onlook-dev/onlook/pull/3062#issuecomment-3650376512 The following files were modified: * `apps/web/client/src/server/api/routers/project/helper.ts` --- .../src/server/api/routers/project/helper.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/web/client/src/server/api/routers/project/helper.ts b/apps/web/client/src/server/api/routers/project/helper.ts index 2216ffaf47..387f78f25f 100644 --- a/apps/web/client/src/server/api/routers/project/helper.ts +++ b/apps/web/client/src/server/api/routers/project/helper.ts @@ -1,6 +1,12 @@ import { eq } from "drizzle-orm"; import { type Frame, projects, userProjects, type DrizzleDb } from "@onlook/db"; +/** + * Extracts the CodeSandbox preview port from an array of frames. + * + * @param frames - Array of frames to search for a CodeSandbox preview URL + * @returns The extracted port number if a matching CodeSandbox preview URL is found, `null` otherwise + */ export function extractCsbPort(frames: Frame[]): number | null { if (!frames || frames.length === 0) return null; @@ -20,8 +26,12 @@ export function extractCsbPort(frames: Frame[]): number | null { } /** - * Verifies that a user has access to a project by checking the userProjects table. - * @throws Error if the user does not have access to the project + * Ensure the specified user has access to the specified project. + * + * @param userId - ID of the user to check + * @param projectId - ID of the project to check + * @throws Error with message 'Project not found' if no project exists with `projectId` + * @throws Error with message 'Unauthorized: You do not have access to this project' if the user has no access to the project */ export async function verifyProjectAccess( db: DrizzleDb, @@ -44,4 +54,4 @@ export async function verifyProjectAccess( if (project.userProjects.length === 0) { throw new Error('Unauthorized: You do not have access to this project'); } -} +} \ No newline at end of file