Skip to content

Security: Cross-Workspace Password Reset IDOR + Schedule Injection + Component Instance IDOR (CWE-639) #369

@lighthousekeeper1212

Description

@lighthousekeeper1212

Summary

Multiple authorization bypass vulnerabilities allow cross-workspace operations. The most critical allows an admin of Workspace A to reset the password of ANY user in the system (including users exclusively in Workspace B), with the new plaintext password returned in the response.

Severity: HIGH (account takeover via cross-workspace password reset)

Finding 1: Cross-Workspace Password Reset (HIGH)

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

userRouter.post('/reset-password', async (req, res) => {
  const workspaceId = getParam(req, 'workspaceId')
  const userId = getParam(req, 'userId')

  if (req.session.userWorkspaces[workspaceId]?.role !== UserWorkspaceRole.admin) {
    res.sendStatus(403)
    return
  }

  const user = await prisma().user.findUnique({
    where: { id: userId },        // <-- No workspace scoping!
    select: { email: true },
  })
  // ...
  await prisma().user.update({
    where: { id: userId },         // <-- No workspace scoping!
    data: { passwordDigest },
  })
  res.json({ password })           // <-- Plaintext password in response
})

The endpoint verifies the caller is admin of the workspace in the URL, but fetches/updates the target user from the global User table by raw ID without verifying the target is a member of that workspace.

Secure comparison (adjacent PUT endpoint in same file):

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

Finding 2: Cross-Workspace Schedule Injection (MEDIUM)

File: apps/api/src/v1/workspaces/workspace/documents/document/schedules/index.ts, lines 19-36

The POST endpoint validates that the URL's :documentId belongs to the workspace, but creates the schedule using documentId from req.body.scheduleParams — an unvalidated value. An attacker can create a schedule targeting a document in another workspace.

Secure comparison: The belongsToDoc middleware on individual schedule routes correctly validates the schedule's documentId matches the URL's documentId.

Finding 3: Component Instance IDOR (LOW-MEDIUM)

File: apps/api/src/v1/workspaces/workspace/components.ts, lines 195-250

  • Create: Neither componentId (URL) nor documentId/blockId (body) validated against workspace
  • Delete: Deletes by blockId alone without workspace check; componentId from URL unused in query

Secure comparison: Adjacent PUT and DELETE on /:componentId correctly validate workspace ownership via component.document.workspaceId !== workspaceId.

Suggested Fix

Finding 1: After fetching user, verify they are a member of the workspace:

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

Finding 2: Use URL's documentId instead of body's when creating schedule.

Finding 3: Add workspace validation matching the parent component endpoints.

Disclosure

Found via static code analysis. Reporting responsibly.

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