Bug: workout-session sync endpoint trusts client-supplied userId (cross-user write)

Author: evan188199-techCreated Jul 7, 2026Updated Jul 7, 2026

Bug

The workout-session sync endpoint (POST /api/workout-sessions/sync) lets an authenticated user write workout data as another user. The route authenticates the caller but then forwards the client-supplied body.session (including body.session.userId) to the sync action unchanged, so a logged-in attacker can sync a session under any userId they choose.

Root cause

app/api/workout-sessions/sync/route.ts:

typescript
const session = await getMobileCompatibleSession(request);
if (!session?.user) {
  return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}
const body = await request.json();
...
// Use the existing server action
const result = await syncWorkoutSessionAction({ session: body.session });  // ← body.session.userId is attacker-controlled

The route validates the caller is authenticated but never replaces body.session.userId with the authenticated session.user.id. The downstream syncWorkoutSessionAction only checks that the supplied userId exists (userExists), not that it matches the caller — so the attacker's userId flows straight into the upsert's userId and the row is created/updated under the victim's account.

Same class of bug as #238 (delete), but on the write path: cross-user workout-data injection/overwrite.

Impact

A logged-in user can post workout sessions that appear under another user's account (and, because the action uses upsert by session id, potentially overwrite an existing session's exercises/sets if they know or guess a id).

Fix

In the sync route, override the client-supplied userId with the authenticated user's id before calling the action:

typescript
const result = await syncWorkoutSessionAction({
  session: { ...body.session, userId: session.user.id },
});

I have a PR ready.