briefercloud / briefercloud/briefer

Cross-workspace password reset IDOR via belongsToWorkspace middleware gap

Open
#368 0 comments 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.