Severity: Critical
Type: Security vulnerability
Scope: Campaigns
Labels: bug, security, Official Campaign
Description
CampaignsService.updateCampaign (src/campaigns/campaigns.service.ts, lines ~53–78) accepts a userId parameter and never uses it. After the findUnique check the method performs prisma.campaign.update on the requested campaignId with no comparison against campaign.creatorId and no admin override:
async updateCampaign(userId: string, campaignId: string, dto: UpdateCampaignDto) {
const campaign = await this.prisma.campaign.findUnique({ where: { id: campaignId } });
if (!campaign) throw new NotFoundException('Campaign not found');
// userId is silently ignored
return this.prisma.campaign.update({ where: { id: campaignId }, data: { /* fields */ } });
}
The controller (src/campaigns/campaigns.controller.ts, lines ~73–93) decorates the route with only @UseGuards(JwtAuthGuard). There is no AdminGuard, no RolesGuard, no per-resource ownership check, and the FORBIDDEN_FIELDS blocklist only filters certain fields — never the caller.
Consequences:
- Any authenticated wallet holder can overwrite
title, description, story, and imageUrl of every campaign on the platform, including featured campaigns.
- Attacker can inject phishing URLs into
story or imageUrl (no sanitisation either).
- Defacing other creators' campaigns requires no admin tooling and leaves no
AuditLog trail.
This is a textbook OWASP A01:2021 Broken Access Control / IDOR vulnerability. Categorised CRITICAL because exploitation is trivial (one authenticated PATCH request) and blast radius is platform-wide.
Recommendation
- In
CampaignsService.updateCampaign, immediately after fetching the campaign, enforce if (campaign.creatorId !== userId && !isAdmin) throw new ForbiddenException(). Mirror the existing deleteUpdate admin-flag pattern.
- Add
@UseGuards(RolesGuard) and @Roles('admin', 'creator') to the controller method, with role normalised from req.user.role like deleteUpdate already does.
- Move
FORBIDDEN_FIELDS from the controller into the DTO using @Exclude() from class-transformer so field protection is declarative.
- Write a regression test that issues
PATCH /campaigns/<id> as wallet A against a campaign owned by wallet B and asserts a 403; do the same for imageUrl because the current absence of validation is what enables phishing injection.
- Add an
AuditLog row for every successful (and rejected) update with actorUserId, targetCampaignId, and the diff applied.
Severity: Critical
Type: Security vulnerability
Scope: Campaigns
Labels:
bug,security,Official CampaignDescription
CampaignsService.updateCampaign(src/campaigns/campaigns.service.ts, lines ~53–78) accepts auserIdparameter and never uses it. After thefindUniquecheck the method performsprisma.campaign.updateon the requestedcampaignIdwith no comparison againstcampaign.creatorIdand no admin override:The controller (
src/campaigns/campaigns.controller.ts, lines ~73–93) decorates the route with only@UseGuards(JwtAuthGuard). There is noAdminGuard, noRolesGuard, no per-resource ownership check, and theFORBIDDEN_FIELDSblocklist only filters certain fields — never the caller.Consequences:
title,description,story, andimageUrlof every campaign on the platform, including featured campaigns.storyorimageUrl(no sanitisation either).AuditLogtrail.This is a textbook OWASP A01:2021 Broken Access Control / IDOR vulnerability. Categorised CRITICAL because exploitation is trivial (one authenticated
PATCHrequest) and blast radius is platform-wide.Recommendation
CampaignsService.updateCampaign, immediately after fetching the campaign, enforceif (campaign.creatorId !== userId && !isAdmin) throw new ForbiddenException(). Mirror the existingdeleteUpdateadmin-flag pattern.@UseGuards(RolesGuard)and@Roles('admin', 'creator')to the controller method, with role normalised fromreq.user.rolelikedeleteUpdatealready does.FORBIDDEN_FIELDSfrom the controller into the DTO using@Exclude()fromclass-transformerso field protection is declarative.PATCH /campaigns/<id>as wallet A against a campaign owned by wallet B and asserts a403; do the same forimageUrlbecause the current absence of validation is what enables phishing injection.AuditLogrow for every successful (and rejected) update withactorUserId,targetCampaignId, and the diff applied.