Skip to content

Cross-workspace password reset IDOR via belongsToWorkspace middleware gap #368

@lighthousekeeper1212

Description

@lighthousekeeper1212

Summary

The belongsToWorkspace middleware in users/index.ts validates that the requesting user is a member of the workspace in the URL path, but does NOT verify that the target userId (also in the URL path) belongs to that same workspace. This allows a workspace admin to reset passwords for users in other workspaces.

Affected Endpoint

POST /api/v1/workspaces/:workspaceId/users/:userId/reset-password

File: apps/api/src/v1/workspaces/workspace/users/user.ts, lines 169-210

The handler fetches the target user by raw ID without workspace scoping:

const user = await prisma().user.findUnique({
  where: { id: userId },  // only checks userId, NOT workspace membership
  select: { email: true },
});
// ... generates new password and updates user

Middleware Gap

File: apps/api/src/v1/workspaces/workspace/users/index.ts, lines 100-119

async function belongsToWorkspace(req, res, next) {
  const workspaceId = getParam(req, 'workspaceId')
  const userId = getParam(req, 'userId')
  const uw = req.session.userWorkspaces[workspaceId]
  if (!uw) {
    res.status(403).end()
    return
  }
  next()  // never checks if :userId belongs to this workspace
}

Secure Pattern (for comparison)

The PUT / (update user) endpoint correctly uses compound workspace scoping:

const user = await prisma().userWorkspace.update({
  where: { userId_workspaceId: { userId, workspaceId } },  // secure
  ...
})

Also Affected

DELETE /:componentId/instances/:blockId in components.ts - deletes by blockId alone without workspace ownership check (while DELETE /:componentId correctly verifies component.document.workspaceId !== workspaceId).

Recommended Fix

Add workspace membership verification for the target userId:

const memberCheck = await prisma().userWorkspace.findUnique({
  where: { userId_workspaceId: { userId, workspaceId } }
});
if (!memberCheck) return res.sendStatus(404);

Reported responsibly. Happy to assist with fixes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions