---
title: "GHSA-3xx2-mqjm-hg9x: Paperclip: Cross-tenant agent API key IDOR in `/agents/:id/keys` routes allows full victim-company compromise"
url: https://erroneo.us/security/ghsa/GHSA-3xx2-mqjm-hg9x/
description: "GHSA-3xx2-mqjm-hg9x (critical, npm): Paperclip: Cross-tenant agent API key IDOR in /agents/:id/keys routes allows full victim-company compromise. Summary…"
type: advisory
section: "Security Digest"
date_published: 2026-04-16
date_modified: 2026-04-16
publisher: erroneo.us
author: erroneo.us Security Desk
ghsa: "GHSA-3xx2-mqjm-hg9x"
license: All rights reserved. Quote with attribution and a link.
---

# GHSA-3xx2-mqjm-hg9x: Paperclip: Cross-tenant agent API key IDOR in `/agents/:id/keys` routes allows full victim-company compromise

## Affected packages

- npm `@paperclipai/server` < 2026.416.0 → fixed: 2026.416.0

## Details

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: }, 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: assertBoard in server/src/routes/authz.ts:4-8 is intentionally narrow: It does not consult req.actor.companyIds or req.actor.isInstanceAdmin. Company-scoping is handled by a separate helper, assertCompanyAccess(req, companyId) (same file, lines 18-31), which the key-management routes never call. The service layer is also unauthenticated. In server/src/services/agents.ts:580-629: Neither the agent id on POST/GET nor the key id on DELETE is cross-checked against the caller's company membership. The returned token becomes a full-fledged agent actor in server/src/middleware/auth.ts:151-169: assertCompanyAccess (lines 22-30 of authz.ts) only rejects an agent actor when req.actor.companyId !== . Because the token the attacker just minted carries the victim's companyId, it sails through every company-access check in Company B — every endpoint that an agent in Company B is authorized to hit. No router-level mitigation exists: api.use(agentRoutes(db)) in server/src/app.ts:155 mounts the router with only boardMutationGuard (which enforces read-only for some board sessions, not tenancy). The adjacent POST /agents/:id/wakeup route at line 2089 and POST /agents/:id/heartbeat/invoke at line 2139 correctly load the agent and call assertCompanyAccess(req, agent.companyId) — the key-management routes simply forgot this check. Commit ac664df8 ("fix(authz): scope import, approvals, activity, and heartbeat routes") hardened several other routes in this same file family but did not…

## Sources

- [GitHub Advisory GHSA-3xx2-mqjm-hg9x](https://github.com/advisories/GHSA-3xx2-mqjm-hg9x)
- [github.com](https://github.com/paperclipai/paperclip/security/advisories/GHSA-3xx2-mqjm-hg9x)

---
Source page: https://erroneo.us/security/ghsa/GHSA-3xx2-mqjm-hg9x/ · Security Digest: https://erroneo.us/security/ · More: https://erroneo.us/llms.txt
