Vulnerability Overview
Vulnerable Code (Taint Tracking)
Source injection: The HTTP request body (json.id) is passed to the tRPC handler
const handler = (req: NextRequest) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => createContext(req),
onError:
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.
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,
};
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 () {
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,
},
});
});
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 Description
curl Example
The attacker is [email protected] and sets cookie values as follows.

# Attacker Cookie Setting
export SB_127_AUTH_TOKEN_0='base64-...0 Value'
export SB_127_AUTH_TOKEN_1='base64-...1 Value'
export COOKIE="sb-127-auth-token.0=${SB_127_AUTH_TOKEN_0}; sb-127-auth-token.1=${SB_127_AUTH_TOKEN_1}"

The victim is [email protected], and the PoC project will be deleted.
