GHSA-XFQJ-R5QW-8G4J
Vulnerability from github – Published: 2026-04-16 22:47 – Updated: 2026-04-16 22:47Summary
Several API endpoints in authenticated mode have no authentication at all. They respond to completely unauthenticated requests with sensitive data or allow state-changing operations. No account, no session, no API key needed.
Verified against the latest version.
Discord: sagi03581
Steps to Reproduce
1. Unauthenticated issue data access
GET /api/heartbeat-runs/:runId/issues returns issue data for a heartbeat run with zero authentication. Every other endpoint in server/src/routes/activity.ts calls assertCompanyAccess, but this one was missed.
curl -s http://<target>:3100/api/heartbeat-runs/00000000-0000-0000-0000-000000000001/issues
# -> [] (HTTP 200, not 401 or 403)
If an attacker obtains a valid run UUID (from logs, error messages, shared URLs, or by probing), they can read issue data without any credentials.
2. Unauthenticated CLI auth challenge creation
POST /api/cli-auth/challenges creates a CLI authentication challenge with no actor check at all. The handler at server/src/routes/access.ts:1638-1659 skips any auth verification.
curl -s -X POST -H "Content-Type: application/json" \
-d '{"command":"test"}' \
http://<target>:3100/api/cli-auth/challenges
# returns challenge ID, token, and a pre-generated board API key
The response includes a boardApiToken that becomes active once the challenge is approved. Combined with open registration (separate report), this enables persistent API key generation.
3. Unauthenticated agent instruction / system prompt leakage
These endpoints in server/src/routes/access.ts require no authentication:
curl -s http://<target>:3100/api/skills/index
# returns all available skill endpoints
curl -s http://<target>:3100/api/skills/paperclip
# returns the FULL agent heartbeat procedure including:
# - every API endpoint and its parameters
# - authentication mechanism (env var names, header formats)
# - the complete agent coordination protocol
# - the agent creation/hiring workflow
curl -s http://<target>:3100/api/skills/paperclip-create-agent
# returns the full agent creation workflow with adapter configs
This hands an attacker a complete map of the internal API without authenticating. It also leaks how agents authenticate, how heartbeats work, and what adapter configurations are available.
4. Unauthenticated deployment configuration disclosure
GET /api/health returns deployment mode, exposure setting, auth status, bootstrap status, version, and feature flags.
curl -s http://<target>:3100/api/health
# {
# "deploymentMode": "authenticated",
# "deploymentExposure": "public",
# "authReady": true,
# "bootstrapStatus": "ready",
# "version": "2026.403.0",
# ...
# }
Tells an attacker exactly how the instance is configured, whether registration is available, and what version is running.
Impact
- Data exposure: heartbeat run issues accessible without credentials. Agent instructions and full API structure exposed to anyone.
- Reconnaissance: an attacker can fingerprint the deployment (mode, version, features) and map the entire internal API before attempting anything else.
- Auth bypass stepping stone: unauthenticated CLI challenge creation is a building block for the full RCE chain (reported separately).
Suggested Fixes
- Add authentication to heartbeat run issues in
server/src/routes/activity.ts: -
GET /api/heartbeat-runs/:runId/issues-- addassertCompanyAccesslike every other endpoint in the same file -
Add authentication to CLI challenge creation in
server/src/routes/access.ts: -
POST /api/cli-auth/challenges-- addassertBoardat minimum -
Add authentication to skill endpoints in
server/src/routes/access.ts: GET /api/skills/availableGET /api/skills/index-
GET /api/skills/:skillName -
Reduce health endpoint information -- consider removing
deploymentMode,deploymentExposure, andversionfrom the unauthenticated response, or gating the full response behindassertBoard -
Consider a global auth rejection middleware for all
/api/*routes inauthenticatedmode. Currently unauthenticated requests getactor: { type: "none" }and pass through tonext(), relying on each route handler to check individually. A missing check means an open endpoint. Rejectingtype: "none"at the middleware level for all routes except an explicit public allowlist (health, sign-in, sign-up, webhooks) would prevent this class of bug entirely.
Contact
Discord: sagi03581
Happy to help verify fixes or provide additional details.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "@paperclipai/server"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2026.416.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-306"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-16T22:47:05Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Summary\n\nSeveral API endpoints in `authenticated` mode have no authentication at all. They respond to completely unauthenticated requests with sensitive data or allow state-changing operations. No account, no session, no API key needed.\n\nVerified against the latest version.\n\nDiscord: sagi03581\n\n## Steps to Reproduce\n\n### 1. Unauthenticated issue data access\n\n`GET /api/heartbeat-runs/:runId/issues` returns issue data for a heartbeat run with zero authentication. Every other endpoint in `server/src/routes/activity.ts` calls `assertCompanyAccess`, but this one was missed.\n\n```bash\ncurl -s http://\u003ctarget\u003e:3100/api/heartbeat-runs/00000000-0000-0000-0000-000000000001/issues\n# -\u003e [] (HTTP 200, not 401 or 403)\n```\n\nIf an attacker obtains a valid run UUID (from logs, error messages, shared URLs, or by probing), they can read issue data without any credentials.\n\n### 2. Unauthenticated CLI auth challenge creation\n\n`POST /api/cli-auth/challenges` creates a CLI authentication challenge with no actor check at all. The handler at `server/src/routes/access.ts:1638-1659` skips any auth verification.\n\n```bash\ncurl -s -X POST -H \"Content-Type: application/json\" \\\n -d \u0027{\"command\":\"test\"}\u0027 \\\n http://\u003ctarget\u003e:3100/api/cli-auth/challenges\n# returns challenge ID, token, and a pre-generated board API key\n```\n\nThe response includes a `boardApiToken` that becomes active once the challenge is approved. Combined with open registration (separate report), this enables persistent API key generation.\n\n### 3. Unauthenticated agent instruction / system prompt leakage\n\nThese endpoints in `server/src/routes/access.ts` require no authentication:\n\n```bash\ncurl -s http://\u003ctarget\u003e:3100/api/skills/index\n# returns all available skill endpoints\n\ncurl -s http://\u003ctarget\u003e:3100/api/skills/paperclip\n# returns the FULL agent heartbeat procedure including:\n# - every API endpoint and its parameters\n# - authentication mechanism (env var names, header formats)\n# - the complete agent coordination protocol\n# - the agent creation/hiring workflow\n\ncurl -s http://\u003ctarget\u003e:3100/api/skills/paperclip-create-agent\n# returns the full agent creation workflow with adapter configs\n```\n\nThis hands an attacker a complete map of the internal API without authenticating. It also leaks how agents authenticate, how heartbeats work, and what adapter configurations are available.\n\n### 4. Unauthenticated deployment configuration disclosure\n\n`GET /api/health` returns deployment mode, exposure setting, auth status, bootstrap status, version, and feature flags.\n\n```bash\ncurl -s http://\u003ctarget\u003e:3100/api/health\n# {\n# \"deploymentMode\": \"authenticated\",\n# \"deploymentExposure\": \"public\",\n# \"authReady\": true,\n# \"bootstrapStatus\": \"ready\",\n# \"version\": \"2026.403.0\",\n# ...\n# }\n```\n\nTells an attacker exactly how the instance is configured, whether registration is available, and what version is running.\n\n## Impact\n\n- **Data exposure**: heartbeat run issues accessible without credentials. Agent instructions and full API structure exposed to anyone.\n- **Reconnaissance**: an attacker can fingerprint the deployment (mode, version, features) and map the entire internal API before attempting anything else.\n- **Auth bypass stepping stone**: unauthenticated CLI challenge creation is a building block for the full RCE chain (reported separately).\n\n## Suggested Fixes\n\n1. **Add authentication to heartbeat run issues** in `server/src/routes/activity.ts`:\n - `GET /api/heartbeat-runs/:runId/issues` -- add `assertCompanyAccess` like every other endpoint in the same file\n\n2. **Add authentication to CLI challenge creation** in `server/src/routes/access.ts`:\n - `POST /api/cli-auth/challenges` -- add `assertBoard` at minimum\n\n3. **Add authentication to skill endpoints** in `server/src/routes/access.ts`:\n - `GET /api/skills/available`\n - `GET /api/skills/index`\n - `GET /api/skills/:skillName`\n\n4. **Reduce health endpoint information** -- consider removing `deploymentMode`, `deploymentExposure`, and `version` from the unauthenticated response, or gating the full response behind `assertBoard`\n\n5. Consider a **global auth rejection middleware** for all `/api/*` routes in `authenticated` mode. Currently unauthenticated requests get `actor: { type: \"none\" }` and pass through to `next()`, relying on each route handler to check individually. A missing check means an open endpoint. Rejecting `type: \"none\"` at the middleware level for all routes except an explicit public allowlist (health, sign-in, sign-up, webhooks) would prevent this class of bug entirely.\n\n## Contact\n\nDiscord: sagi03581\n\nHappy to help verify fixes or provide additional details.",
"id": "GHSA-xfqj-r5qw-8g4j",
"modified": "2026-04-16T22:47:05Z",
"published": "2026-04-16T22:47:05Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/paperclipai/paperclip/security/advisories/GHSA-xfqj-r5qw-8g4j"
},
{
"type": "PACKAGE",
"url": "https://github.com/paperclipai/paperclip"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L",
"type": "CVSS_V3"
}
],
"summary": "Paperclip: Unauthenticated Access to Multiple API Endpoints in Authenticated Mode"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.