briefercloud / briefercloud/briefer
Security: Cross-Workspace Password Reset IDOR + Schedule Injection + Component Instance IDOR (CWE-639)
- Dominant language
- TypeScript
- Stars
- 4.3k
- Forks
- 290
- PR merge metrics
- No merged PRs in 30d
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
```typescript
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):
```typescript
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:
```typescript
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.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.