[Regression on closed #537] PATCH /teams/:id/splits still has no ownership check — any SPONSOR or MAINTAINER can rewrite any team's payout percentages
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 55/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
- Domain
- authorization, backend-api-design, payments
Research direction
Start with src/teams/teams.controller.ts and src/teams/teams.service.ts, then compare updateSplits with assignToBounty to trace how caller identity and authorization are handled. Check how team ownership and bounty status are represented before implementing the requested checks. Add controller and service specs for forbidden callers, and confirm owners and maintainers can still update splits while teams past CLAIMED cannot be changed.
Written by the indexing model from the issue text.
Description
Location — src/teams/teams.controller.ts:65 (updateSplits) and src/teams/teams.service.ts:72 (TeamsService.updateSplits).
Problem — The defect closed as #537 is present again on the default branch. The route is guarded only by @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.MAINTAINER, UserRole.SPONSOR), which answers "is this caller a sponsor/maintainer", not "does this caller own this team":
updateSplits(
@Param('id', new ParseUUIDPipe()) id: string,
@Body() dto: UpdateTeamSplitsDto,
) {
return this.teamsService.updateSplits(id, dto.splits);
}
The caller's identity is not even passed to the service — there is no @CurrentUser() parameter — and TeamsService.updateSplits(teamId, members) takes no caller id, so no check is possible downstream either. Team.createdById exists on the entity and is set at creation time (teams.service.ts:41), but nothing ever reads it. Compare assignToBounty (teams.service.ts:108), which does receive callerId and does check sponsor-or-maintainer against the specific bounty.
Why it matters — This is a direct payout-hijack path on money. Any account holding the SPONSOR or MAINTAINER role can enumerate team UUIDs and rewrite another team's member splits to [{ userId: <attacker>, percentage: 100 }]. The next markMergedAndRelease on a bounty assigned to that team pays the whole bounty amount to the attacker instead of to the contributors who did the work. Nothing in the flow re-validates split ownership at payout time, and the existing splits are destroyed (splitRepo.delete({ teamId }), line 90), so the original percentages are not recoverable from the database.
Reproduction / failure path
- User A (SPONSOR) creates team T with members B 60% / C 40%.
POST /teamsreturns T's id. - User D, an unrelated account that merely holds the SPONSOR role, calls
PATCH /teams/<T>/splitswith body{"splits":[{"userId":"<D>","percentage":100}]}. - Returns 200. T's splits are now 100% to D. No ownership error, and A is not notified.
- A bounty assigned to T is merged and released; the entire payout goes to D.
Suggested fix
- Add
@CurrentUser() user: AuthenticatedUserto the controller method and passuser(id and roles) intoTeamsService.updateSplits. - In the service, after
findOne(teamId), assertteam.createdById === callerIdor that the caller holdsUserRole.MAINTAINER, mirroring the check inassignToBounty(teams.service.ts:118-128), and throwForbiddenExceptionotherwise. - Also reject the mutation when the team is already attached to a bounty whose status is past
CLAIMED, so splits cannot be rewritten after work has been delivered. - Add a controller spec and a service spec asserting 403 for a non-owner non-maintainer caller — #537's fix had no such test, which is why it could silently regress.
- Dominant language
- TypeScript
- Stars
- 2
- Forks
- 90
- Avg merge
- 31m
- Merged PRs (30d)
- 20
Getting set up
- Ships a Dockerfile or Docker Compose file
- No pull request template
- No contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from MergeFi/backend
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
Maintainers usually reply within 1 day
-
bug security
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day
-
bug testing
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
Maintainers usually reply within 1 day
-
bug security
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
zapcooking/frontend#946 ·
Maintainers usually reply within 1 day
-
enhancement good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
WiliamRosa/cosmos-semantic-platform#40 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
liberation-data/drivine#733 · 1 comment ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
makecindy/cindy#10062 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day