Paperclip: Cross-tenant agent API key IDOR in `/agents/:id/keys` routes allows full victim-company compromise
Date: 2026-04
Affected: npm/@paperclipai/server
Attack vector: auth-bypass
Category: vulnerability-disclosure
Severity: Critical
CWE: CWE-639
CVSS: 9.9 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)

## Summary The `GET`, `POST`, and `DELETE` handlers under `/agents/:id/keys` in the Paperclip control-plane API only call `assertBoard(req)`, which verifies that the caller has a board-type session but does not verify that the caller has access to the company owning the target agent. A board user whose membership is limited to Company A can therefore list, create, or revoke agent API keys for any agent in Company B by supplying the victim agent's UUID in the URL path. The `POST` handler returns the newly-minted token in cleartext, which authenticates subsequent requests as `{type:"agent", companyId:<CompanyB>}`, giving the attacker full agent-level access inside the victim tenant — a complete cross-tenant compromise. ## Details The three vulnerable routes are defined in `server/src/routes/agents.ts:2050-2087`: ```ts router.get("/agents/:id/keys", async (req, res) => { assertBoard(req); // <-- only checks actor.type === "board" const id = req.params.id as string; const keys = await svc.listKeys(id); res.json(keys); }); router.post("/agents/:id/keys", validate(createAgentKeySchema), async (req, res) => { assertBoard(req); // <-- same const id = req.params.id as string; const key = await svc.createApiKey(id, req.body.name); // ... activity log ... res.status(201).json(key); // returns cleartext `token` }); router.delete("/agents/:id/keys/:keyId", async (req, res) => { assertBoard(req);


Mitigations:

Architectural layers (MAESTRO):