IntersectMBO / IntersectMBO/cc-portal

🐛 [Bounty] - IDOR via Path/Body ID Mismatch in UserPathGuard (3 Endpoints Affected)

Open
#635 2 comments 0 reactions 1 assignee Claimed by @l-br1 View on GitHub
🐛 Bug
Dominant language
TypeScript
Stars
5
Forks
1
PR merge metrics
No merged PRs in 30d

Description

### Domain

constitution.gov.tools

### Context

**This report has been received through the security mailbox.**

## Description

### Summary
`UserPathGuard` validates the URL path `:id` against the caller's JWT, but three controller methods forward `body.userId` to the business logic instead of the validated path parameter. This confused deputy pattern allows any authenticated admin to deactivate accounts, delete users, or escalate privileges for arbitrary users.

### Details
**Root cause**: `users-path.guard.ts` lines 10-23 -- validates path `:id` but business logic uses `body.userId`

**Affected endpoints**:
1. `PATCH /users/:id/toggle-status` -- `users.controller.ts` lines 263-275
2. `DELETE /users/:id` -- `users.controller.ts` lines 292-307
3. `PATCH /users/:id/role-permissions` -- `users.controller.ts` lines 331-341

The `UserPathGuard` enforces `[request.params.id](http://request.params.id/) === request.user.userId` -- meaning the URL path `:id` must be the caller's own user ID. This was intended to prevent users from acting on other users' resources.

However, all three affected controller methods ignore the validated path parameter and pass the request body to the facade, which reads `userId` from the body:

```typescript
// toggleStatus -- path id validated by guard, body.userId used by logic
async toggleStatus(@Param('id') id: string, @Body() toggleStatusRequest: ToggleStatusRequest) {
return await this.usersFacade.toggleStatus(toggleStatusRequest, permissions);
// facade calls: findById(toggleStatusRequest.userId) -- NOT path id
}
```

The same pattern repeats for `removeUser` and `updateUserRoleAndPermissions`.

**Attack**: Put your own ID in the URL path (passes guard), put the victim's ID in the body (used by business logic).

### Steps to reproduce

### PoC
**Deactivate any user (toggle-status):**
1. Authenticate as admin `AAAA` with `MANAGE_CC_MEMBERS` permission.
2. Send: `PATCH /users/AAAA/toggle-status` with body `{ "userId": "VICTIM", "status": "inactive" }`
3. UserPathGuard checks `AAAA === AAAA` -> passes.
4. Facade calls `findById("VICTIM")` -> deactivates victim.

**Escalate privileges (role-permissions):**
1. Authenticate as admin `AAAA` with `MANAGE_ROLES_AND_PERMISSIONS`.
2. Send: `PATCH /users/AAAA/role-permissions` with body `{ "userId": "ALLY", "roleId": "", "permissions": [...all...] }`
3. Guard passes. Service grants ally full permissions (super-admin target is the only protection).

**Delete any user:**
1. Send: `DELETE /users/AAAA` with body `{ "userId": "VICTIM" }`
2. The self-delete safety check (`id !== body.userId`) actually *helps* the attacker -- AAAA != VICTIM passes.

### Actual behavior

### Impact
- Any admin can deactivate, delete, or modify permissions of any other non-super-admin user.
- Enables privilege escalation: grant yourself or allies elevated permissions.
- Enables targeted disruption: deactivate other CC members to influence quorum.
- The `UserPathGuard` provides zero effective authorization on affected endpoints.

In the context of the Constitutional Committee portal, this enables a single compromised or malicious admin to manipulate the committee's membership and voting power.

### Expected behavior

### Remediation
Use the validated path `id` in business logic, not the body:

```diff
async toggleStatus(
@Param('id', ParseUUIDPipe) id: string,
@Body() toggleStatusRequest: ToggleStatusRequest,
): Promise {
- return await this.usersFacade.toggleStatus(toggleStatusRequest, permissions);
+ return await this.usersFacade.toggleStatus(id, toggleStatusRequest.status, permissions);
}
```

Or replace `UserPathGuard` entirely with proper RBAC that validates the caller's permissions against the target user at the service layer.

## Affected Products
- **Ecosystem**: `npm`
- **Package name**: `cc-portal`
- **Affected versions**: `<= current`
- **Patched versions**: `(none)`

## Severity
- **Dropdown**: `High`
- **Vector string**: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H
- **Score**: 8.1

## Weaknesses
- **CWE**: CWE-639 -- Authorization Bypass Through User-Controlled Key

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.