All vulnerabilities
GHSA-9c7g-grf7-j2r5

com.arcadedb:arcadedb-engine: Read-only ArcadeDB users can change edge type lightweight/unique schema flags via ALTER TYPE WITH, bypassing UPDATE_SCHEMA

com.arcadedb:arcadedb-engine
CVSS
4.3Medium
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in26.10.1Fixed versionGitHub
Published5 Oct 2026Updated 5 Oct 2026GitHub

What fixes it

  • 26.10.1com.arcadedb:arcadedb-engine, mavenGitHub<dependency><groupId>com.arcadedb</groupId><artifactId>arcadedb-engine</artifactId><version>26.10.1</version></dependency>

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

Description

Summary

An authenticated ArcadeDB user who holds only read access to a database can permanently change the lightweight and unique storage declarations of an edge type by issuing a single SQL command, ALTER TYPE <edgeType> WITH lightweight = true, over the HTTP command API (or the gRPC and Bolt equivalents). The database engine gates schema mutation behind the UPDATE_SCHEMA permission, enforced by a checkForSchemaMutation() call inside each schema-mutating method. The two edge-type setters LocalEdgeType.setLightweight() and LocalEdgeType.setUnique() omit that call, and the bare WITH-only form of ALTER TYPE produces an empty item list, so the per-item authorization path never runs. A read-only identity therefore crosses the documented UPDATE_SCHEMA boundary and persists an unauthorized schema change.

Root Cause

The engine enforces schema-mutation authorization per method: LocalDocumentType.checkForSchemaMutation() calls checkPermissionsOnDatabase(UPDATE_SCHEMA), and every schema-mutating operation on LocalDocumentType (rename, addSuperType, removeSuperType, setAliases, addBucket, removeBucket, setBucketSelectionStrategy, setCustomValue, createProperty, and others) invokes it. The edge-type-specific setters do not, and there is no compensating statement-level or transport-level check on the ALTER TYPE ... WITH path.

engine/src/main/java/com/arcadedb/schema/LocalEdgeType.java
  - setLightweight(boolean)  (line 65): mutates the lightweight flag; no checkForSchemaMutation() call
  - setUnique(boolean)       (line 74): mutates the unique flag; no checkForSchemaMutation() call

engine/src/main/java/com/arcadedb/query/sql/parser/AlterTypeStatement.java
  - executeDDL(CommandContext) (line 232): the per-item loop (line 241) that would reach an
    authorized method runs zero times for the bare WITH-only form; the trailing settings block
    (lines 266-291) then calls the two unguarded setters and persists via saveConfiguration().

Impact

An attacker needs a single authenticated database account with no more than read access (for example a read-only API token) and network reach to the command/query endpoint, which is enabled by default. With one request the attacker flips an edge type's lightweight or unique declaration and the change is persisted to the schema configuration, surviving a server restart. This breaches the documented permission model, which advertises UPDATE_SCHEMA as the gating right for schema mutation. The demonstrated capability is limited to two boolean edge-type flags: the attacker cannot create or drop types, alter arbitrary schema, or read or write record data through this path. Changing the lightweight declaration alters how future edges of that type are stored, which can plausibly affect application-level correctness for code that relies on the declared storage shape; this consequence follows from the flag change but was not executed against a running application.

Proof of Concept

Preconditions:
  - A running ArcadeDB server (version in the affected range) with security enabled.
  - An edge type named FriendOf that currently holds no edge records.
  - An attacker account "reader" granted only read access to the target database
    (no updateSchema grant).
  1. As an administrator, create the edge type and the read-only user (setup only, not part of the attack).
  2. As the read-only user, send the bare WITH-only ALTER TYPE command shown below.
  3. Observe that the command succeeds and that the lightweight flag is now persisted, even though the same user is refused ordinary schema mutations such as CREATE VERTEX TYPE.
# Executed as the read-only attacker account
curl -s -u reader:reader-password \
  -H 'Content-Type: application/json' \
  -X POST http://TARGET:2480/api/v1/command/mydb \
  -d '{"language":"sql","command":"ALTER TYPE FriendOf WITH lightweight = true"}'

# Negative control: a genuine schema mutation is correctly refused for the same user
curl -s -u reader:reader-password \
  -H 'Content-Type: application/json' \
  -X POST http://TARGET:2480/api/v1/command/mydb \
  -d '{"language":"sql","command":"CREATE VERTEX TYPE ShouldFail"}'
First command: HTTP 200 with result {"lightweight": true}; schema.json now records the
  edge type as lightweight.
Second command: rejected with a security/permission error, confirming the account lacks
  UPDATE_SCHEMA and that the first command bypassed the same gate.

Attack Chain

  1. Exposure: The attacker reaches the default-enabled command endpoint (HTTP POST /api/v1/command/{database}, or the gRPC/Bolt command entry points) with a valid read-only credential for the target database.
  2. Control: The attacker fully controls the SQL command text and chooses the bare ALTER TYPE <edgeType> WITH lightweight = true (or unique = ...) form, which the grammar accepts with no preceding ALTER items.
  3. Path: AlterTypeStatement.executeDDL() iterates its items list to apply each item; for the WITH-only form that list is empty, so the loop (and the per-item authorization it would reach) runs zero times. Execution falls through to the trailing settings block, which calls edgeType.setLightweight(...) and/or edgeType.setUnique(...) and then schema.saveConfiguration().
  4. Guard: The UPDATE_SCHEMA check (checkForSchemaMutation() / checkPermissionsOnDatabase(UPDATE_SCHEMA)) is absent from both LocalEdgeType.setLightweight() and LocalEdgeType.setUnique(), and no statement-level or transport-level equivalent exists on this path. The only conditional in setLightweight() is a SchemaException raised when enabling lightweight on an already-populated type, which is a data-integrity precondition, not an authorization check.
  5. Primitive: The attacker reaches and completes a persisted schema mutation (the edge-type lightweight/unique declaration).
  6. Result: A read-only account crosses the UPDATE_SCHEMA privilege boundary and permanently changes an edge type's storage declaration. setLightweight(false) is unconditionally unguarded regardless of record population; setLightweight(true) succeeds on empty types; setUnique is unguarded in scenarios where the secondary index constraint helper returns early (a lightweight type, a pre-existing backing index, or setUnique(false) when no index exists).

Bypass Evidence

The relevant guard is the engine's per-method UPDATE_SCHEMA enforcement. It is genuinely missing on this path rather than present-but-bypassable at the method level:

  • A text search of LocalEdgeType.java returns zero occurrences of checkForSchemaMutation or checkPermissionsOnDatabase; the two setters are plain field assignments.
  • Every analogous schema-mutating setter on the parent LocalDocumentType does call checkForSchemaMutation(), so the omission on the two edge-type setters is the specific gap.
  • The per-item authorization is reached only through the applyItem() loop, which iterates the parsed items list; the code comment at AlterTypeStatement.java:155 states that the grammar allows the bare ALTER TYPE <name> WITH <settings> form with no items, so the loop executes zero times and the settings block runs unguarded.

Attempts to disprove the finding and their outcomes: (a) searched for a statement-level or transport-level UPDATE_SCHEMA check across the DDL base class, the HTTP handler, and the Bolt/gRPC command handlers, and found none; auth is delegated entirely to engine methods. (b) Checked whether an inherited method supplies the check for the edge-type setters; the setters are defined directly on LocalEdgeType with no super-call to a guarded method. (c) Tested whether the SchemaException in setLightweight() blocks the attack; it only fires for setLightweight(true) on populated types and is a data check, leaving setLightweight(false) and the empty-type case unguarded. (d) Examined the secondary index helpers applyUniqueConstraint()/removeUniqueConstraint() as compensating checks for setUnique; they guard only some scenarios and return early in others, and the flag itself is set before they run.

This is not a duplicate, incomplete-fix, or regression of the prior schema-authorization advisory GHSA-vg6x-6pg9-6qwg (CVE-2026-54076), which scoped its fix to LocalDocumentType and LocalProperty. The LocalEdgeType.setLightweight()/setUnique() methods and the ALTER TYPE ... WITH lightweight/unique feature did not exist at the time of that fix (release 26.6.1); they were introduced in 26.8.1, and this is an independent code path with a distinct remediation boundary.

Affected Versions

  • Ecosystem: maven
  • Package: com.arcadedb:arcadedb-engine
  • Confirmed affected range: >= 26.8.1, <= 26.9.1
  • Latest release checked: 26.9.1 (Maven Central, com.arcadedb:arcadedb-engine, verified against the published sources JAR)
  • Fix status: not fixed (the unreleased development branch remains vulnerable)

The vulnerable setters and the ALTER TYPE ... WITH lightweight/unique feature are absent from 26.7.3 and earlier and first appear in 26.8.1; both 26.8.1 and 26.9.1 contain the unguarded setters. No released version enforces UPDATE_SCHEMA on this path, and no fix is present on the current development branch.

Suggested Fix

Enforce the UPDATE_SCHEMA invariant on the edge-type declaration path so that every persisted schema mutation is authorized regardless of statement form. The direct fix is to call checkForSchemaMutation() at the start of both LocalEdgeType.setLightweight() and LocalEdgeType.setUnique(), matching every other schema-mutating setter on LocalDocumentType. Alternatively, or in addition as defense in depth, add a checkPermissionsOnDatabase(UPDATE_SCHEMA) call in AlterTypeStatement.executeDDL() before the trailing settings block so the WITH-only form is covered even if new settings are added later. The remediation must cover both true and false values, the empty-type and populated-type cases, and the setUnique scenarios where the index-constraint helpers return early. As an interim mitigation, grant write access only to trusted users and API tokens and treat all schema DDL as administrator-only at the application layer until upgraded.

Reported by zx (GitHub: @manus-pi).

Text from GitHub Advisory Database

Affected versions 1

  • com.arcadedb:arcadedb-engine, maven>= 26.8.1, <= 26.9.1GitHub