-
Notifications
You must be signed in to change notification settings - Fork 290
Description
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) nordocumentId/blockId(body) validated against workspace - Delete: Deletes by
blockIdalone without workspace check;componentIdfrom 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.