VYPR
Critical severity9.0NVD Advisory· Published May 12, 2026· Updated May 13, 2026

CVE-2026-44221

CVE-2026-44221

Description

ArcadeDB is a Multi-Model DBMS. Prior to 2.6.4, authenticated users and API tokens scoped to a specific database could read, write, and mutate schema on any other database on the same server. Two distinct defects contributed: (1) ServerSecurityUser.getDatabaseUser() returned a DB user with an uninitialized fileAccessMap, which requestAccessOnFile treated as allow-all; (2) ArcadeDBServer.createDatabase() omitted factory.setSecurity(...) so any database created via POST /api/v1/server {"command":"create database X"} had its entire record-level authorization system silently disabled. In combination, record-level and database-level authorization could be bypassed by any authenticated principal. This vulnerability is fixed in 2.6.4.

Patches

1
04110c06315d

fix: enforce per-database access in HTTP command handler

https://github.com/ArcadeData/arcadedbLuca GarulliApr 20, 2026via ghsa
4 files changed · +84 7
  • engine/src/main/java/com/arcadedb/schema/LocalDocumentType.java+4 0 modified
    @@ -18,6 +18,7 @@
      */
     package com.arcadedb.schema;
     
    +import com.arcadedb.database.DatabaseInternal;
     import com.arcadedb.database.Document;
     import com.arcadedb.database.MutableDocument;
     import com.arcadedb.database.RecordEvents;
    @@ -35,6 +36,7 @@
     import com.arcadedb.index.TypeIndex;
     import com.arcadedb.index.lsm.LSMTreeIndexAbstract;
     import com.arcadedb.log.LogManager;
    +import com.arcadedb.security.SecurityDatabaseUser;
     import com.arcadedb.serializer.json.JSONObject;
     import com.arcadedb.utility.CollectionUtils;
     import com.arcadedb.utility.FileUtils;
    @@ -398,6 +400,8 @@ public LocalProperty createProperty(final String propertyName, final Type proper
        */
       @Override
       public LocalProperty createProperty(final String propertyName, final Type propertyType, final String ofType) {
    +    ((DatabaseInternal) schema.getDatabase()).checkPermissionsOnDatabase(SecurityDatabaseUser.DATABASE_ACCESS.UPDATE_SCHEMA);
    +
         if (properties.containsKey(propertyName))
           throw new SchemaException(
               "Cannot create the property '" + propertyName + "' in type '" + name + "' because it already exists");
    
  • server/src/main/java/com/arcadedb/server/ArcadeDBServer.java+2 0 modified
    @@ -474,6 +474,8 @@ public ServerDatabase createDatabase(final String databaseName, final ComponentF
               configuration.getValueAsString(GlobalConfiguration.SERVER_DATABASE_DIRECTORY) + File.separator
                   + databaseName).setAutoTransaction(true);
     
    +      factory.setSecurity(getSecurity());
    +
           if (factory.exists())
             throw new IllegalArgumentException("Database '" + databaseName + "' already exists");
     
    
  • server/src/main/java/com/arcadedb/server/security/ServerSecurity.java+39 6 modified
    @@ -638,12 +638,45 @@ public ServerSecurityUser authenticateByApiToken(final String tokenValue) {
     
       protected JSONObject getDatabaseGroupsConfiguration(final String databaseName) {
         final JSONObject groupDatabases = groupRepository.getGroups().getJSONObject("databases");
    -    JSONObject databaseConfiguration = groupDatabases.has(databaseName) ? groupDatabases.getJSONObject(databaseName) : null;
    -    if (databaseConfiguration == null)
    -      // GET DEFAULT (*) DATABASE GROUPS
    -      databaseConfiguration = groupDatabases.has(SecurityManager.ANY) ? groupDatabases.getJSONObject("*") : null;
    -    if (databaseConfiguration == null || !databaseConfiguration.has("groups"))
    +
    +    // When the caller asks for the wildcard itself, return the wildcard live reference directly — callers
    +    // (setup helpers, reload tests) rely on mutating the returned object.
    +    if (SecurityManager.ANY.equals(databaseName)) {
    +      final JSONObject wildcard = groupDatabases.has(SecurityManager.ANY) ? groupDatabases.getJSONObject("*") : null;
    +      return wildcard != null && wildcard.has("groups") ? wildcard.getJSONObject("groups") : null;
    +    }
    +
    +    final JSONObject wildcardConfiguration = groupDatabases.has(SecurityManager.ANY) ? groupDatabases.getJSONObject("*") : null;
    +    final JSONObject specificConfiguration = groupDatabases.has(databaseName) ? groupDatabases.getJSONObject(databaseName) : null;
    +
    +    final JSONObject wildcardGroups =
    +        wildcardConfiguration != null && wildcardConfiguration.has("groups") ? wildcardConfiguration.getJSONObject("groups") : null;
    +    final JSONObject specificGroups =
    +        specificConfiguration != null && specificConfiguration.has("groups") ? specificConfiguration.getJSONObject("groups") : null;
    +
    +    if (wildcardGroups == null && specificGroups == null)
           return null;
    -    return databaseConfiguration.getJSONObject("groups");
    +
    +    // When there is no db-specific entry, return the wildcard groups live reference so existing callers that mutate it
    +    // (e.g. setup helpers doing getDatabaseGroupsConfiguration(db).put(groupName, ...)) keep working.
    +    if (specificGroups == null)
    +      return wildcardGroups;
    +    if (wildcardGroups == null)
    +      return specificGroups;
    +
    +    // MERGE: wildcard groups are the baseline, db-specific entries override same-named groups. The returned object is
    +    // a snapshot copy; callers that need to persist groups must go through saveGroup/deleteGroup.
    +    final JSONObject merged = new JSONObject();
    +    for (final String groupName : wildcardGroups.keySet()) {
    +      final Object value = wildcardGroups.get(groupName);
    +      if (value instanceof JSONObject)
    +        merged.put(groupName, value);
    +    }
    +    for (final String groupName : specificGroups.keySet()) {
    +      final Object value = specificGroups.get(groupName);
    +      if (value instanceof JSONObject)
    +        merged.put(groupName, value);
    +    }
    +    return merged;
       }
     }
    
  • server/src/test/java/com/arcadedb/server/security/CrossDatabaseAccessIT.java+39 1 modified
    @@ -93,6 +93,39 @@ void scopedApiTokenCannotReadOtherDatabase() throws Exception {
         });
       }
     
    +  @Test
    +  void readOnlyApiTokenCannotInsertOnOwnDatabase() throws Exception {
    +    // Mirrors the exact repro sent by the reporter (Art): a read-only token scoped to 'live' must not be able to
    +    // INSERT INTO Memory on the very same 'live' database. Token payload uses no 'database' key inside permissions,
    +    // matching the reporter's minimised token-only repro.
    +    testEachServer((serverIndex) -> {
    +      createDatabase(serverIndex, OTHER_DB);
    +      try {
    +        createType(serverIndex, OTHER_DB, "Memory");
    +
    +        final JSONObject permissions = new JSONObject()
    +            .put("types", new JSONObject()
    +                .put("*", new JSONObject().put("access", new JSONArray().put("readRecord"))));
    +        final String token = createTokenForDatabase(serverIndex, "art-readonly-token", OTHER_DB, permissions);
    +        final String tokenAuth = "Bearer " + token;
    +
    +        final int readStatus = commandStatus(serverIndex, OTHER_DB, tokenAuth, "SELECT FROM Memory");
    +        assertThat(readStatus).as("read-only token must be allowed to SELECT on its own database").isEqualTo(200);
    +
    +        final int insertStatus = commandStatus(serverIndex, OTHER_DB, tokenAuth,
    +            "INSERT INTO Memory SET content = 'token-write-attempt', salience = 0.2");
    +        assertThat(insertStatus).as("read-only token must not INSERT on its own database").isGreaterThanOrEqualTo(400);
    +
    +        final int schemaStatus = commandStatus(serverIndex, OTHER_DB, tokenAuth,
    +            "CREATE PROPERTY Memory.scope_probe_api_token STRING");
    +        assertThat(schemaStatus).as("read-only token must not mutate schema on its own database").isGreaterThanOrEqualTo(400);
    +      } finally {
    +        deleteToken(serverIndex, "art-readonly-token");
    +        dropDatabase(serverIndex, OTHER_DB);
    +      }
    +    });
    +  }
    +
       @Test
       void scopedApiTokenCannotWriteOtherDatabase() throws Exception {
         testEachServer((serverIndex) -> {
    @@ -159,6 +192,11 @@ private String createCrudToken(final int serverIndex, final String name) throws
       }
     
       private String createToken(final int serverIndex, final String name, final JSONObject permissions) throws Exception {
    +    return createTokenForDatabase(serverIndex, name, getDatabaseName(), permissions);
    +  }
    +
    +  private String createTokenForDatabase(final int serverIndex, final String name, final String database,
    +      final JSONObject permissions) throws Exception {
         final ApiTokenConfiguration tokenConfig = getServer(serverIndex).getSecurity().getApiTokenConfiguration();
         tokenConfig.listTokens().stream()
             .filter(t -> name.equals(t.getString("name", "")))
    @@ -170,7 +208,7 @@ private String createToken(final int serverIndex, final String name, final JSONO
     
         final JSONObject payload = new JSONObject();
         payload.put("name", name);
    -    payload.put("database", getDatabaseName());
    +    payload.put("database", database);
         payload.put("expiresAt", 0);
         payload.put("permissions", permissions);
         connection.getOutputStream().write(payload.toString().getBytes());
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.