Vulnerability Description


Vulnerability Overview

Vulnerable Code (Taint Tracking)

  1. Source injection: The HTTP request body (json.id) is passed to the tRPC handler

    https://github.com/onlook-dev/onlook/blob/8770e5460c30bbfa612cbe948a1632f9f93ba43e/apps/web/client/src/app/api/trpc/[trpc]/route.ts#L17-L23

    const handler = (req: NextRequest) =>
        fetchRequestHandler({
            endpoint: '/api/trpc',
            req,
            router: appRouter,
            createContext: () => createContext(req),
            onError:
    
  2. Context creation: User is injected via cookies Only the user is injected via the session cookie; authorization for a specific project.id is not verified at this stage.

    https://github.com/onlook-dev/onlook/blob/8770e5460c30bbfa612cbe948a1632f9f93ba43e/apps/web/client/src/server/api/trpc.ts#L31-L47

    export const createTRPCContext = async (opts: { headers: Headers }) => {
        const supabase = await createClient();
        const {
            data: { user },
            error,
        } = await supabase.auth.getUser();
    
        if (error) {
            throw new TRPCError({ code: 'UNAUTHORIZED', message: error.message });
        }
    
        return {
            db,
            supabase,
            user,
            ...opts,
        };
    
  3. Auth middleware passed: login only (no authorization) It checks only the “logged-in” status and lets the request through. Because there is no guard to verify resource ownership/membership, an arbitrary id can be used as-is in subsequent steps. https://github.com/onlook-dev/onlook/blob/8770e5460c30bbfa612cbe948a1632f9f93ba43e/apps/web/client/src/server/api/trpc.ts#L131-L150

    export const protectedProcedure = t.procedure.use(timingMiddleware).use(({ ctx, next }) => {
        if (!ctx.user) {
            throw new TRPCError({ code: 'UNAUTHORIZED' });
        }
    
        if (![ctx.user.email](<http://ctx.user.email>)) {
            throw new TRPCError({
                code: 'UNAUTHORIZED',
                message: 'User must have an email address to access this resource',
            });
        }
    
        return next({
            ctx: {
                // infers the `session` as non-nullable
                user: ctx.user as SetRequiredDeep<User, 'email'>,
                db: ctx.db,
            },
        });
    });
    
  4. Sink reached: Immediate deletion using input id (no ownership verification) Using only input.id, it deletes directly from projects and userProjects. There is no guard to confirm whether the “current user owns the project or has membership.” https://github.com/onlook-dev/onlook/blob/8770e5460c30bbfa612cbe948a1632f9f93ba43e/apps/web/client/src/server/api/routers/project/project.ts#L355-L362

    delete: protectedProcedure
        .input(z.object({ id: z.string() }))
        .mutation(async ({ ctx, input }) => {
            await ctx.db.transaction(async (tx) => {
                await tx.delete(projects).where(eq([projects.id](<http://projects.id>), [input.id](<http://input.id>)));
                await tx.delete(userProjects).where(eq(userProjects.projectId, [input.id](<http://input.id>)));
            });
        }),
    

PoC


PoC Description

curl Example