---
title: "GHSA-47wq-cj9q-wpmp: Paperclip: Cross-tenant agent API token minting via missing assertCompanyAccess on /api/agents/:id/keys"
url: https://erroneo.us/security/ghsa/GHSA-47wq-cj9q-wpmp/
description: "GHSA-47wq-cj9q-wpmp (critical, npm): Paperclip: Cross-tenant agent API token minting via missing assertCompanyAccess on /api/agents/:id/keys. > Isolated…"
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-47wq-cj9q-wpmp"
license: All rights reserved. Quote with attribution and a link.
---

# GHSA-47wq-cj9q-wpmp: Paperclip: Cross-tenant agent API token minting via missing assertCompanyAccess on /api/agents/:id/keys

## Affected packages

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

## Details

> Isolated paperclip instance running in authenticated mode (default config) > on a clean Docker image matching commit b649bd4 (2026.411.0-canary.8, post > the 2026.410.0 patch). This advisory was verified on an unmodified build. Summary POST /api/agents/:id/keys, GET /api/agents/:id/keys, and DELETE /api/agents/:id/keys/:keyId (server/src/routes/agents.ts lines 2050-2087) only call assertBoard to authorize the caller. They never call assertCompanyAccess and never verify that the caller is a member of the company that owns the target agent. Any authenticated board user (including a freshly signed-up account with zero company memberships and no instanceadmin role) can mint a plaintext pcp agent API token for any agent in any company on the instance. The minted token is bound to the victim agent's companyId server-side, so every downstream assertCompanyAccess check on that token authorizes operations inside the victim tenant. This is a pure authorization bypass on the core tenancy boundary. It is distinct from GHSA-68qg-g8mg-6pr7 (the unauth import → RCE chain disclosed in 2026.410.0): that advisory fixed one handler, this report is a different handler with the same class of mistake that the 2026.410.0 patch did not cover. Root Cause server/src/routes/agents.ts, lines 2050-2087: Compare the handler 12 lines below, router.post("/agents/:id/wakeup"), which shows the correct pattern: it fetches the agent, then calls assertCompanyAccess(req, agent.companyId). The three /keys handlers above do not even fetch the agent. The token returned by POST /agents/:id/keys is bound to the victim company in server/src/services/agents.ts, lines 580-609: actorMiddleware (server/src/middleware/auth.ts) then resolves the bearer token to actor = { type: "agent", companyId: existing.companyId }, so every subsequent assertCompanyAccess(req, victim.companyId) check passes. The exact same assertBoard-only pattern is also present on agent lifecycle handlers in the same file (POST /agents/:id/pause, /resume, /terminate, and DELETE /agents/:id at lines 1962, 1985, 2006, 2029). An attacker can terminate, delete, or silently pause any agent in any company with the…

## Sources

- [GitHub Advisory GHSA-47wq-cj9q-wpmp](https://github.com/advisories/GHSA-47wq-cj9q-wpmp)
- [github.com](https://github.com/paperclipai/paperclip/security/advisories/GHSA-47wq-cj9q-wpmp)

---
Source page: https://erroneo.us/security/ghsa/GHSA-47wq-cj9q-wpmp/ · Security Digest: https://erroneo.us/security/ · More: https://erroneo.us/llms.txt
