Measured on origin/main (d271b2f7); line numbers below are at the ADR-017 section's stamp 6a262fe8, where the route sits at routes/activity.ts:164.
POST /api/activity/create is gated by auth alone. It destructures type, action, content, podId, target, agentMetadata straight off the request body and calls Activity.create with them. There is no pod-membership or pod-existence check — podId is whatever the caller sends.
The body carries no approval subdoc, and it does not need to. models/Activity.ts:111 declares:
approval: {
status: { type: String, enum: ['pending', 'approved', 'rejected'], default: 'pending' },
Mongoose materialises that default on every document, so a row created with type: 'approval_needed' satisfies both clauses of the reader:
activitySchema.statics.getPendingApprovals = async function (podIds) {
return this.find({ podId: { $in: podIds }, type: 'approval_needed', 'approval.status': 'pending', deleted: { $ne: true } })
ActivityService.getPendingApprovals resolves podIds from pods the viewer created or admins — so the row lands in the decision queue of that pod's admins, attributed to the caller as actor, with attacker-controlled content.
Repro shape: authenticate as any user, POST /api/activity/create with { type: 'approval_needed', action: 'approval_needed', content: '<anything>', podId: '<any pod id>' }, then read GET /api/activity/approvals as an admin of that pod.
Why this is filed now. ADR-017 (#1256) stated that seedPodActivities was the only code that ever creates an approval_needed row; that sentence is corrected in 3bcbca61. The designed producer, Activity.createApprovalRequest, still has zero callers — so today the queue is empty and nothing has been injected. #1298 is currently wiring board facts into the same "Needs you" surface, which is why the producer set is worth having right.
Same class as the PATCH /api/integrations/:id finding: a generic create/merge endpoint is a writer for every schema-declared key, and a schema default can supply the half the caller did not send.
Suggested fix: validate podId against the caller's membership, and reject type: 'approval_needed' on this route entirely — approval rows should only come from Activity.createApprovalRequest.
🤖 Generated with Claude Code
Measured on
origin/main(d271b2f7); line numbers below are at the ADR-017 section's stamp6a262fe8, where the route sits atroutes/activity.ts:164.POST /api/activity/createis gated byauthalone. It destructurestype,action,content,podId,target,agentMetadatastraight off the request body and callsActivity.createwith them. There is no pod-membership or pod-existence check —podIdis whatever the caller sends.The body carries no
approvalsubdoc, and it does not need to.models/Activity.ts:111declares:Mongoose materialises that default on every document, so a row created with
type: 'approval_needed'satisfies both clauses of the reader:ActivityService.getPendingApprovalsresolvespodIdsfrom pods the viewer created or admins — so the row lands in the decision queue of that pod's admins, attributed to the caller as actor, with attacker-controlledcontent.Repro shape: authenticate as any user,
POST /api/activity/createwith{ type: 'approval_needed', action: 'approval_needed', content: '<anything>', podId: '<any pod id>' }, then readGET /api/activity/approvalsas an admin of that pod.Why this is filed now. ADR-017 (#1256) stated that
seedPodActivitieswas the only code that ever creates anapproval_neededrow; that sentence is corrected in3bcbca61. The designed producer,Activity.createApprovalRequest, still has zero callers — so today the queue is empty and nothing has been injected. #1298 is currently wiring board facts into the same "Needs you" surface, which is why the producer set is worth having right.Same class as the
PATCH /api/integrations/:idfinding: a generic create/merge endpoint is a writer for every schema-declared key, and a schema default can supply the half the caller did not send.Suggested fix: validate
podIdagainst the caller's membership, and rejecttype: 'approval_needed'on this route entirely — approval rows should only come fromActivity.createApprovalRequest.🤖 Generated with Claude Code