{"schema":"threatline.record/2","record":{"id":"GHSA-hw9x-xx38-rg28","aliases":["GHSA-hw9x-xx38-rg28"],"summary":"ArcadeDB Bolt protocol: missing per-database authorization lets an authenticated user read another database's schema metadata","details":"## Summary\n\nThe ArcadeDB Bolt wire protocol handler (`com.arcadedb:arcadedb-bolt`) never performs a per-database authorization check when a client selects or switches the active database. A user who holds valid credentials for any single database on the server can send a standard Bolt `RUN` or `BEGIN` message whose `db` field names a database they were not granted, then read that database's full schema metadata (all node and relationship type names, all property keys, and all index and constraint definitions) using the built-in system procedures the handler answers directly. Actual record data stays protected by a deny-all sentinel, but the schema metadata read path bypasses that sentinel entirely. Exploitation needs only network access to the Bolt port (7687, bound to all interfaces by default) and credentials for one database.\n\n## Root Cause\n\nEvery other ArcadeDB wire protocol gates database selection with `canAccessToDatabase()`: MongoDB, Redis, PostgreSQL, gRPC, and the HTTP handler family all check the caller's grant before serving a named database. The Bolt handler does not. `BoltNetworkExecutor.ensureDatabase()` resolves the client-supplied database name and opens it through `server.getDatabase(targetName)` with no authorization call anywhere on the path. The only security-relevant action it takes is binding a deny-all `ServerSecurityDatabaseUser` sentinel to the database context, which rejects record-level operations routed through the engine ACL layer (`requestAccessOnDatabase`, `requestAccessOnFile`, `requestAccessOnType`).\n\nHowever, `handleSystemQuery()` intercepts system queries (`CALL db.labels()`, `CALL db.relationshipTypes()`, `CALL db.propertyKeys()`, `SHOW INDEXES`, `SHOW CONSTRAINTS`) before the engine's ACL-gated execution path is ever reached, and answers them by reading the schema through the direct Java API (`database.getSchema().getTypes()`). Those direct reads never invoke the ACL layer, so the deny-all sentinel has no effect on them. The security invariant that a user may only observe databases they are authorized for is therefore violated for schema metadata.\n\n```text\nAffected functions (bolt/src/main/java/com/arcadedb/bolt/BoltNetworkExecutor.java):\n  ensureDatabase()      - opens a user-named database with no canAccessToDatabase() check\n  handleSystemQuery()   - serves schema metadata via direct Schema API, bypassing the ACL sentinel\nSupporting evidence: bolt/src/main/java/com/arcadedb/bolt/BoltSystemProcedures.java\n  (serveSchemaProcedure has zero security imports or permission checks)\nContrast (authorization is enforced on the SQL path):\n  engine .../FetchFromSchemaTypesStep.java calls SecurityHelper.canAccessType(user, type, READ_RECORD)\n```\n\n## Impact\n\nAn attacker who can authenticate to the Bolt endpoint with credentials for any one database (`PR:L`) can enumerate the complete schema of any other database hosted on the same server instance, regardless of their grants. Disclosed information includes every vertex (node) type name, every edge (relationship) type name, every property key name, and every index and constraint definition (index type, indexed properties, uniqueness, provider). Schema names frequently reveal sensitive data categories and stored fields (for example type names such as `CreditCard` or property names such as `ssn`), providing precise reconnaissance for a follow-on attack. This is a confidentiality impact limited to metadata: the deny-all sentinel continues to block reads and writes of actual record data through the engine, so no record contents, no write capability, and no availability impact were demonstrated (`C:L/I:N/A:N`). The scope is unchanged because the Bolt server is reading its own managed databases under its own security authority (`S:U`).\n\n## Proof of Concept\n\nRequires: a running ArcadeDB server with the Bolt protocol enabled (default), at least two databases, and a non-root user granted access to only one of them. The official Neo4j Python driver drives the Bolt `db` field via the `database=` session parameter.\n\nSetup:\n1. Start an ArcadeDB server (Bolt listens on 7687 by default).\n2. Create two databases, `public_db` and `secret_db`.\n3. Create a user granted access to `public_db` only.\n\n```python\nfrom neo4j import GraphDatabase\n\n# Credentials the attacker legitimately holds for public_db only.\ndriver = GraphDatabase.driver(\"bolt://TARGET_HOST:7687\", auth=(\"limited_user\", \"PASSWORD\"))\n\n# Target a database the user was NOT granted by naming it in the session.\nwith driver.session(database=\"secret_db\") as s:\n    print(\"labels:\",  [r.data() for r in s.run(\"CALL db.labels()\")])\n    print(\"relTypes:\", [r.data() for r in s.run(\"CALL db.relationshipTypes()\")])\n    print(\"propKeys:\", [r.data() for r in s.run(\"CALL db.propertyKeys()\")])\n    print(\"indexes:\",  [r.data() for r in s.run(\"SHOW INDEXES\")])\n```\n\n```text\nExpected: the CALL db.* and SHOW INDEXES results return the schema of secret_db\n(type names, property keys, index and constraint definitions) even though\nlimited_user has no grant on secret_db. A direct MATCH (n) RETURN n on the same\nsession is blocked by the deny-all sentinel, confirming record data stays protected\nwhile schema metadata leaks.\n```\n\n## Attack Chain\n\n1. **Exposure:** The Bolt protocol plugin is auto-registered via Java ServiceLoader and ships in the standard distribution; `BOLT_PORT` defaults to 7687 and `BOLT_HOST` to `0.0.0.0`, so the endpoint is network-reachable by default.\n2. **Control:** The attacker authenticates with valid credentials for a database they are granted, then fully controls the `db` field of a subsequent `RUN` or `BEGIN` message. Only emptiness is validated (`db != null && !db.isEmpty()`).\n3. **Path:** `handleRun()` / `handleBegin()` copy the attacker's `db` value into `databaseName`, then call `ensureDatabase()`, which detects the switch, drops the current handle, and opens the named database via `server.getDatabase(targetName)`.\n4. **Guard:** No authorization guard exists on this path; `canAccessToDatabase()` is never called (contrast MongoDB, Redis, PostgreSQL, gRPC, HTTP, all of which check). The deny-all sentinel that is bound only gates engine-routed record operations, not direct schema reads.\n5. **Primitive:** The attacker issues `CALL db.labels()`, `CALL db.relationshipTypes()`, `CALL db.propertyKeys()`, `SHOW INDEXES`, or `SHOW CONSTRAINTS`, which `handleSystemQuery()` answers by reading `database.getSchema()` directly, before the ACL-gated engine path.\n6. **Result:** The complete schema metadata of a database the attacker holds no grant for is serialized back as Bolt records.\n\n## Bypass Evidence\n\nThe relevant guard is the deny-all `ServerSecurityDatabaseUser` sentinel bound in `ensureDatabase()`. It is a real, effective control for record-level operations: `requestAccessOnDatabase()`, `requestAccessOnFile()`, and `requestAccessOnType()` all return false for the unauthorized database, so `MATCH`/CRUD through the engine is blocked. The bypass is that schema metadata reads do not travel that route. `handleSystemQuery()` returns before the engine's ACL-gated execution path, and the schema procedures read `database.getSchema().getTypes()` directly; `LocalSchema.getTypes()` returns the full type set unconditionally, and `BoltSystemProcedures` contains no security imports or permission checks. That the SQL path (`FetchFromSchemaTypesStep`) does call `SecurityHelper.canAccessType()` confirms the project intends schema reads to be authorization-filtered; the Bolt system-query path omits that filter.\n\nAdversarial disproof attempts that failed: (1) `server.getDatabase()` performs no authorization; it is a plain map lookup. (2) Authentication does not re-check on database switch; `ensureDatabase()` contains no `authenticate()` or `canAccessToDatabase()` call. (3) The deny-all sentinel does not cover schema reads, as traced above. (4) Bolt is enabled by default, not an opt-in module. (5) Both `RUN` and `BEGIN` accept the `db` switch. Every other protocol handler checks `canAccessToDatabase()`; Bolt is the sole exception (`grep -c canAccessToDatabase BoltNetworkExecutor.java` = 0).\n\n## Affected Versions\n\n- `Ecosystem: maven`\n- `Package: com.arcadedb:arcadedb-bolt`\n- `Confirmed affected range: >= 26.2.1, <= 26.9.1`\n- `Latest release checked: 26.9.1 (Maven Central / GitHub release tag)`\n- `Fix status: not fixed (no fix on any released version or on the current development branch)`\n\nThe Bolt module was introduced in 26.2.1; the vulnerable code is present unchanged from that release through the latest release, 26.9.1. The related cross-database authorization advisory GHSA-x8mg-6r4p-87pf was fixed in 26.7.2, but that fix targeted the HTTP handler family in `com.arcadedb:arcadedb-server` and never touched the Bolt module (`git log --all --grep=GHSA-x8mg -- bolt/` returns nothing). The single commit that modified the Bolt executor between 26.7.2 and 26.9.1 added pre-authentication connection gating, not per-database authorization. No fixed release exists.\n\n## Suggested Fix\n\nEnforce the same per-database authorization invariant on the Bolt path that the other protocols already apply. In `ensureDatabase()`, before opening a client-named database via `server.getDatabase(targetName)`, verify `user.canAccessToDatabase(targetName)` and fail closed (returning a Bolt security failure) when the grant is absent, including the deferred-selection case where the database name is only supplied on a later `RUN`/`BEGIN`. Because the schema system-query path reads outside the engine ACL layer, the authorization check must gate database selection itself rather than relying on the deny-all sentinel; alternatively, route the `handleSystemQuery()` schema reads through the same `SecurityHelper.canAccessType()` filter the SQL path uses. As an interim mitigation without a code change, restrict Bolt port exposure to trusted networks and grant users only on servers where cross-database schema visibility is acceptable.\n\n---\n\nReported by zx (GitHub: @manus-pi).\n","categories":["Unclassified"],"product":"com.arcadedb:arcadedb-bolt","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-hw9x-xx38-rg28","source_modified_at":"2026-10-05T20:05:26Z","fetched_at":"2026-10-28T20:58:01.013507Z","package":{"ecosystem":"maven","name":"com.arcadedb:arcadedb-bolt"},"range":">= 26.2.1, <= 26.9.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-hw9x-xx38-rg28","source_modified_at":"2026-10-05T20:05:26Z","fetched_at":"2026-10-28T20:58:01.013507Z","package":{"ecosystem":"maven","name":"com.arcadedb:arcadedb-bolt"},"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:26Z","modified_at":"2026-10-05T20:05:26Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-hw9x-xx38-rg28","fetched_at":"2026-10-28T20:58:01.013507Z","source_modified_at":"2026-10-05T20:05:26Z"}],"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:26Z","content_hash":"5a5c543e40fccdef42fbfac2f4378757f4c1edb8f2a6006af0d97354c87b82dc"}}
