fix(workout-session): enforce auth and ownership in sync action (CWE-862/IDOR) - #245
Open
andesyteoss wants to merge 1 commit into
Open
andesyteoss wants to merge 1 commit into
andesyteoss wants to merge 1 commit into
Conversation
…sion sync The syncWorkoutSessionAction previously used the unauthenticated actionClient and trusted the userId supplied in the request body. Any caller could sync (create/overwrite) workout sessions for arbitrary users (IDOR / CWE-862). Switch to authenticatedActionClient and reject requests where session.userId does not match the authenticated user's id.
|
@sebastiondev is attempting to deploy a commit to the Workoutcool Team Team on Vercel. A member of the Team first needs to authorize it. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📝 Description
This PR fixes a missing-authorization / IDOR vulnerability in the
syncWorkoutSessionActionserver action (src/features/workout-session/actions/sync-workout-sessions.action.ts).The problem
The action was exported from the unauthenticated
actionClient:Two issues stacked:
actionClientinsrc/shared/api/safe-actions.tsdoes not require a session, whereasauthenticatedActionClientin the same file does (.use(async ({ next }) => { const user = await getUser(); return next({ ctx: { user } }); })). Any unauthenticated caller could invoke the action.userIdtrusted from the request body — theuserIdused for theworkoutSessionwrite comes straight fromparsedInput.session.userId. It was never compared against the caller's identity.The only server-side check was
prisma.user.findUnique({ where: { id: session.userId } })— an existence check, not an ownership check. Anyone who knows or guesses another user's ID could:id(the action does aprisma.workoutSession.upsertkeyed onsession.id, which replacesexercises/setsviadeleteMany: {}+create).This maps to CWE-862 (Missing Authorization) and IDOR.
The fix
Minimal, single-file change:
authenticatedActionClient, which runs the existinggetUser()middleware and populatesctx.user.session.userId !== ctx.user.id, before touching Prisma.prisma.user.findUniqueexistence check becomes redundant once ownership is enforced (the caller is the user, and their existence is already guaranteed bygetUser()), so it's removed to keep the change tight.Relevant diff:
No schema changes, no migrations, no API-surface change for legitimate callers (the client already sends its own
userId; that value now just has to match the authenticated session).Proof of concept
Assuming a running dev server and the pre-fix code, the action is invoked through
next-safe-action's HTTP transport:Pre-fix: a row is written to
WorkoutSessionwithuserId = $VICTIM_ID. Supplying an existingsession.idoverwrites the victim's session (upsertupdatebranch doesexercises: { deleteMany: {} }and re-creates them).Post-fix: request is rejected with
serverError: USER_NOT_FOUNDunless the caller is the victim. In-app, legitimate clients callsyncWorkoutSessionAction({ session })wheresession.userIdalready comes from the current user, so nothing breaks.📋 Checklist
authenticatedActionClientprimitive)🗃️ Prisma Migrations (if applicable)
Testing
src/shared/api/safe-actions.tsthatauthenticatedActionClientthrowsActionError("Session is required!")/"Session is not valid!"when no user is present, so unauthenticated calls now short-circuit before reaching the handler.ctx.user.idis populated from server-sidegetUser(), so it cannot be spoofed by the client.userIdcontinues to work —session.userId === ctx.user.idholds and the upsert proceeds unchanged.ctxis correctly inferred from the middleware.Adversarial review
Before submitting, we tried to disprove this finding. Two potential mitigations were considered: (1) an upstream auth middleware that might have gated
actionClientanyway — but the two clients are explicitly distinct insrc/shared/api/safe-actions.ts, and onlyauthenticatedActionClientchainsgetUser(); and (2) whether Prisma-level constraints would block cross-user writes — they don't,WorkoutSession.userIdis a plain foreign key with no row-level policy. Theprisma.user.findUniqueexistence check only proves the target user exists, not that the caller is them. The vulnerability is real and the fix closes it at the action boundary, which is the correct layer for this kind of authorization check in anext-safe-actioncodebase.🔗 Related Issues
None filed — reporting via PR per the open-source disclosure norm.