{"schema":"threatline.record/2","record":{"id":"GHSA-9c7g-grf7-j2r5","aliases":["GHSA-9c7g-grf7-j2r5"],"summary":"Read-only ArcadeDB users can change edge type lightweight/unique schema flags via ALTER TYPE WITH, bypassing UPDATE_SCHEMA","details":"## Summary\n\nAn 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.\n\n## Root Cause\n\nThe 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.\n\n```text\nengine/src/main/java/com/arcadedb/schema/LocalEdgeType.java\n  - setLightweight(boolean)  (line 65): mutates the lightweight flag; no checkForSchemaMutation() call\n  - setUnique(boolean)       (line 74): mutates the unique flag; no checkForSchemaMutation() call\n\nengine/src/main/java/com/arcadedb/query/sql/parser/AlterTypeStatement.java\n  - executeDDL(CommandContext) (line 232): the per-item loop (line 241) that would reach an\n    authorized method runs zero times for the bare WITH-only form; the trailing settings block\n    (lines 266-291) then calls the two unguarded setters and persists via saveConfiguration().\n```\n\n## Impact\n\nAn 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.\n\n## Proof of Concept\n\n```text\nPreconditions:\n  - A running ArcadeDB server (version in the affected range) with security enabled.\n  - An edge type named FriendOf that currently holds no edge records.\n  - An attacker account \"reader\" granted only read access to the target database\n    (no updateSchema grant).\n```\n\n1. As an administrator, create the edge type and the read-only user (setup only, not part of the attack).\n2. As the read-only user, send the bare `WITH`-only ALTER TYPE command shown below.\n3. 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`.\n\n```bash\n# Executed as the read-only attacker account\ncurl -s -u reader:reader-password \\\n  -H 'Content-Type: application/json' \\\n  -X POST http://TARGET:2480/api/v1/command/mydb \\\n  -d '{\"language\":\"sql\",\"command\":\"ALTER TYPE FriendOf WITH lightweight = true\"}'\n\n# Negative control: a genuine schema mutation is correctly refused for the same user\ncurl -s -u reader:reader-password \\\n  -H 'Content-Type: application/json' \\\n  -X POST http://TARGET:2480/api/v1/command/mydb \\\n  -d '{\"language\":\"sql\",\"command\":\"CREATE VERTEX TYPE ShouldFail\"}'\n```\n\n```text\nFirst command: HTTP 200 with result {\"lightweight\": true}; schema.json now records the\n  edge type as lightweight.\nSecond command: rejected with a security/permission error, confirming the account lacks\n  UPDATE_SCHEMA and that the first command bypassed the same gate.\n```\n\n## Attack Chain\n\n1. **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.\n2. **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.\n3. **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()`.\n4. **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.\n5. **Primitive:** The attacker reaches and completes a persisted schema mutation (the edge-type `lightweight`/`unique` declaration).\n6. **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).\n\n## Bypass Evidence\n\nThe 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:\n\n- A text search of `LocalEdgeType.java` returns zero occurrences of `checkForSchemaMutation` or `checkPermissionsOnDatabase`; the two setters are plain field assignments.\n- 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.\n- 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.\n\nAttempts 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.\n\nThis 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.\n\n## Affected Versions\n\n- `Ecosystem: maven`\n- `Package: com.arcadedb:arcadedb-engine`\n- `Confirmed affected range: >= 26.8.1, <= 26.9.1`\n- `Latest release checked: 26.9.1 (Maven Central, com.arcadedb:arcadedb-engine, verified against the published sources JAR)`\n- `Fix status: not fixed (the unreleased development branch remains vulnerable)`\n\nThe 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.\n\n## Suggested Fix\n\nEnforce 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.\n\nReported by zx (GitHub: @manus-pi).","categories":["Unclassified"],"product":"com.arcadedb:arcadedb-engine","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-9c7g-grf7-j2r5","source_modified_at":"2026-10-05T20:05:24Z","fetched_at":"2026-10-23T19:58:35.668311Z","package":{"ecosystem":"maven","name":"com.arcadedb:arcadedb-engine"},"range":">= 26.8.1, <= 26.9.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-9c7g-grf7-j2r5","source_modified_at":"2026-10-05T20:05:24Z","fetched_at":"2026-10-23T19:58:35.668311Z","package":{"ecosystem":"maven","name":"com.arcadedb:arcadedb-engine"},"version":"26.10.1"}],"cvss":4.3,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-05T20:05:24Z","modified_at":"2026-10-05T20:05:24Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-9c7g-grf7-j2r5","fetched_at":"2026-10-23T19:58:35.668311Z","source_modified_at":"2026-10-05T20:05:24Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-05T20:05:24Z","content_hash":"bf49f798303f2305d9ae04aa5818f643306e289e5007732c938355058f55dd12"}}
