Skip to content

fix: enforce ownership on campaign update (IDOR)#26

Open
Adeolu01 wants to merge 4 commits into
OrbitChainLabs:mainfrom
Adeolu01:fix/campaign-update-broken-access-control
Open

fix: enforce ownership on campaign update (IDOR)#26
Adeolu01 wants to merge 4 commits into
OrbitChainLabs:mainfrom
Adeolu01:fix/campaign-update-broken-access-control

Conversation

@Adeolu01

Copy link
Copy Markdown
Contributor

Closes #19

CampaignsService.updateCampaign accepted a userId but never used it, so any authenticated wallet holder could overwrite the title, description, story, and imageUrl of every campaign — a textbook OWASP A01:2021 Broken Access Control / IDOR, with imageUrl/story usable for phishing injection and no audit trail.

Changes:

  • Enforce per-resource ownership in updateCampaign: reject with 403 Forbidden unless the caller is the campaign creator or an admin, mirroring the existing deleteUpdate admin-flag pattern.
  • Fix the controller passing the wrong identity (req.user.id, which is undefined) — now uses req.user.sub and derives isAdmin from req.user.role.
  • Write an AuditLog row (CAMPAIGN_UPDATED) on every successful update recording actor, target campaign, and the applied diff.
  • Add regression tests asserting 403 when wallet A edits a campaign owned by wallet B (including the coverImageUrl/phishing case), plus owner-success and admin-override coverage.

Note: @Roles('CREATOR','ADMIN') was intentionally not added to the route — campaign creation does not promote a USER to CREATOR, so that decorator would lock legitimate owners out of their own campaigns. Per-resource ownership is the correct boundary.

@Alqku

Alqku commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

@Adeolu01 please fix the CI thanks.

Alqku commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Clean, focused IDOR fix — the ownership check + audit log is exactly the right shape. Thanks for picking this up 🙌

Alqku commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Heads up: this now has a merge conflict on src/campaigns/campaigns.service.ts (and a couple of related files) after we landed #32. Could you rebase onto current main and push again? Once the conflict is resolved and CI is clean, I\u2019ll merge right away \u270d\ufe0f

Adeolu01 added 3 commits July 1, 2026 08:18
…-broken-access-control

# Conflicts:
#	src/campaigns/campaigns.service.spec.ts
The Jest e2e step declared two env mappings, which is invalid workflow
syntax and caused GitHub Actions to reject the whole file so no jobs ran.
Merge them into a single env block that points at the postgres and redis
service containers and keeps the PORT value.
The e2e suite passes, but the booted Nest app holds open handles (Redis,
Bull queues, schedulers) so jest never exits on its own. The job then hung
until the 15-minute timeout and was reported as cancelled. Enable forceExit
in the e2e jest config so the process ends once the tests finish.
@Adeolu01

Adeolu01 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

@Alqku done. I merged current main into the branch, resolved the conflict, and CI is now fully green.

Summary of what was needed:

  • Resolved the src/campaigns/campaigns.service.spec.ts add/add conflict by keeping both suites side by side: the updateCampaign access-control tests from this PR and the milestone target-validation tests from main. All access-control assertions (403 IDOR, phishing coverImageUrl, owner success, admin override) are intact.
  • The other campaign files (campaigns.service.ts, campaigns.controller.ts) merged cleanly, so both the IDOR fix and the milestone validation from main coexist.
  • Fixed two pre-existing CI issues that were blocking every job:
    • The workflow file had a duplicate env: key on the Jest e2e step, which made Actions reject the whole file so no jobs ran. Merged it into a single valid env block.
    • The e2e suite passed but Jest never exited (the booted Nest app holds open Redis/Bull/scheduler handles), so the job hung until the 15-minute timeout and was reported as cancelled. Enabled forceExit in the e2e Jest config so the process ends once tests finish.

All six checks (Build, Lint, Format, Prisma validate, unit, e2e) are passing and the branch is mergeable. Ready for merge whenever you are.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[CRITICAL] — PATCH /campaigns/:id is an IDOR: any authenticated wallet holder can overwrite *any* campaign's title, description, story, and image

2 participants