Skip to content

POST /api/activity/create lets any authenticated user inject a row into any pod's approval queue #1300

Description

@lilyshen0722

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 checkpodId 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

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