briefercloud / briefercloud/briefer
Cross-workspace password reset IDOR via belongsToWorkspace middleware gap
- Dominant language
- TypeScript
- Stars
- 4.3k
- Forks
- 290
- PR merge metrics
- No merged PRs in 30d
Description
## 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:
```typescript
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
```typescript
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:
```typescript
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:
```typescript
const memberCheck = await prisma().userWorkspace.findUnique({
where: { userId_workspaceId: { userId, workspaceId } }
});
if (!memberCheck) return res.sendStatus(404);
```
*Reported responsibly. Happy to assist with fixes.*
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with apps/api/src/v1/workspaces/workspace/users/index.ts and user.ts, focusing on belongsToWorkspace and POST /api/v1/workspaces/:workspaceId/users/:userId/reset-password; compare them with the secure PUT / endpoint's compound workspace scoping. Also inspect components.ts and DELETE /:componentId/instances/:blockId. Done means both affected operations enforce workspace ownership before changing or deleting data.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, authorization, backend, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100