Skip to content

Activity approve/reject routes have no membership or owner check — any authenticated user can resolve any approval #1452

Description

@lilyshen0722

POST /api/activity/:activityId/approve and /reject carry no authorization beyond auth. Any authenticated user who knows an activity id can approve or reject an approval_needed activity in a pod they are not a member of.

Measured at origin/main 9ee1fac6d, not read off the source.

Reproduction

Caller is a member of no pod; the activity belongs to pod-1, whose createdBy and members are someone-else.

APPROVE status=200 approveCalled=1 body={"success":true,"status":"approved"}
CONTROL/create status=403 createCalled=0 body={"error":"Only pod members can create activities in a pod"}

The /create line is the positive control, run in the same harness against the same mocked pod and caller. It refuses and never reaches the model — so the harness can see a guard, and approve's 200 is the route's behaviour rather than a bypassed mock. activity.approve was called with the attacker's own userId, so the row records them as the approver.

Where the gap is

routes/activity.ts requires isPodMember and applies it on exactly two routes:

  • :231POST /seed/:podId
  • :258POST /create

:197 /:activityId/approve and :211 /:activityId/reject are auth-only. The service behind them does not compensate: ActivityService.approveActivity / rejectActivity do Activity.findById, check activity.type !== 'approval_needed', and call activity.approve(userId, notes). Neither loads the pod.

__tests__/unit/routes/activity.write-membership.test.js covers /create and /seed/:podId only — the file's own header names the create hole as the one that mattered, and the approve/reject pair was not in that sweep.

Why it matters

This is the resolve verb for ADR-020 cards, whose D3 states cards are actionable by the workspace owner only, and TASK-095 v1.1 is currently being specced with resolveApproval as the single resolve verb for clickable options. A ruling surface that anyone can press is worth closing before more UI points at it.

Not caused by, but adjacent to, #1451

#1451 is a correct fix to a separate dead-predicate bug in getPendingApprovals and should not be held for this. It does widen which users see pending approvals (creator-only → creator + members), which surfaces more activity ids to more people; those people are legitimate pod members, so it is exposure rather than escalation. The hole here is independent of it and predates it.

Suggested fix

Load the activity's pod and apply the same isPodMember check the sibling write routes use — plus whatever narrower owner rule ADR-020 D3 actually intends, since "workspace owner only" is stricter than pod membership. Pin both with a non-member test and a member positive control.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions