Skip to content

Bug: deleteWorkoutSessionAction has no auth/ownership check — any user can delete others sessions #238

Description

@evan188199-tech

Bug

deleteWorkoutSessionAction lets any authenticated user delete any other user's workout session. It's built on the unauthenticated actionClient, fetches the session's userId but never compares it to the caller, and deletes by id alone.

Root cause

src/features/workout-session/actions/delete-workout-session.action.ts:

export const deleteWorkoutSessionAction = actionClient.schema(...).action(async ({ parsedInput }) => {
  const { id } = parsedInput;
  const session = await prisma.workoutSession.findUnique({
    where: { id },
    select: { userId: true },     // ← fetched, but never compared to anyone
  });
  if (!session) { ... }
  await prisma.workoutSession.delete({ where: { id } });  // ← deletes unconditionally
  ...
});

actionClient (vs authenticatedActionClient) performs no auth and exposes no ctx.user, so the action has no idea who the caller is. The session row's userId is selected and then discarded. Any logged-in user who sends a valid session id deletes it — cross-user data loss.

The codebase already has authenticatedActionClient (used by favorites/get-users) that injects ctx.user via serverAuth(); the delete action just doesn't use it.

Impact

A user can delete another user's workout history by id (guessable/enumerable cuids, leaked via leaderboard/session-share). No audit, no ownership check.

Fix

Switch to authenticatedActionClient and require ownership:

export const deleteWorkoutSessionAction = authenticatedActionClient
  .schema(deleteWorkoutSessionSchema)
  .action(async ({ parsedInput, ctx }) => {
    const { id } = parsedInput;
    const session = await prisma.workoutSession.findUnique({ where: { id }, select: { userId: true } });
    if (!session || session.userId !== ctx.user.id) {
      return { serverError: "Session not found" };
    }
    await prisma.workoutSession.delete({ where: { id } });
    return { success: true };
  });

Returning the same "not found" for both missing and non-owned prevents existence-disclosure. I have a PR ready.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions