GHSA-p7mm-r948-4q3q
## Summary The approval-resolution endpoints (`POST /approvals/:id/approve`, `/reject`, `/request-revision`) accept a client-supplied `decidedByUserId` field in the request body and write it verbatim into the authoritative `approvals.decidedByUserId` column — without cross-checking it against the authenticated actor. Any board user who can access an approval's company can record the decision as having been made by another user (e.g. the CEO), forging the governance audit trail. For `hire_agent` approvals with a monthly budget, the same attacker-controlled string is also stamped onto the resulting `budget_policies` row as `createdByUserId`/`updatedByUserId`. ## Details **Entry point** — `server/src/routes/approvals.ts:130`: ```ts router.post("/approvals/:id/approve", validate(resolveApprovalSchema), async (req, res) => { assertBoard(req); const id = req.params.id as string; if (!(await requireApprovalAccess(req, id))) { res.status(404).json({ error: "Approval not found" }); return; } const { approval, applied } = await svc.approve( id, req.body.decidedByUserId ?? "board", // ← client-controlled req.body.decisionNote, ); ``` **Authorization check** — `server/src/routes/authz.ts:4`: ```ts export function assertBoard(req: Request) { if (req.actor.type !== "board") { throw forbidden("Board access required"); } } ``` `assertBoard` only checks that the caller is some board user; it never ties `req.body.decidedByUserId` to `req.actor.userId`. `requireApprovalAccess`/`assertCompanyAccess` only verify the attacker is allowed to touch the approval's company, which every board user in that company already is. **Validator** — `packages/shared/src/validators/approval.ts:13`: ```ts export const resolveApprovalSchema = z.object({ decisionNote: z.string().optional().nullable(), decidedByUserId: z.string().optional().default("board"), }); ``` The Zod schema accepts any string for `decidedByUserId` — no UUID check, no membership
Properties
- ghsa_id
- GHSA-p7mm-r948-4q3q
- severity
- medium
- summary
- Paperclip: Approval decision attribution spoofing via client-controlled `decidedByUserId` in paperclip server
- cvss_score
- 4.3
- cve_id
- GHSA-p7mm-r948-4q3q
- cvss_vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
- is_ghsa_only
- true
- ghsa_published
- 2026-04-16T22:48:46Z
- source_url
- https://github.com/advisories/GHSA-p7mm-r948-4q3q
- ghsa_updated
- 2026-04-16T22:48:47Z
Related Entities (4)
VULNERABLE_TO (1)
AFFECTS (1)
HAS_WEAKNESS (1)
REPORTED_BY (1)
Explore deeper with Ninja Signal's threat intelligence graph