Vulnerability GHSA-93cm-2v4m-c56c

Medium Risk
MEDIUM RISK
CVSS Score: 6.5
Score Range: 4.0–6.9
Medium severity vulnerabilities (CVSS 4.0–6.9). Important issues that meaningfully reduce security confidence.
4 hours ago
November 04, 2026 at 05:02 PM UTC
Arc: DuckDB I/O aliases bypass database RBAC after CVE-2026-47735
<0.0.0-20260610131716-5dc8f53b70a3
<0.0.0-20260610131716-5dc8f53b70a3

Summary

Arc: DuckDB I/O aliases bypass database RBAC after CVE-2026-47735

Details

Summary

The fix for CVE-2026-47735 (released 2026.06.1) is incomplete. It added a DuckDB sandbox (SET GLOBAL allowed_directories=[<storage root>]; SET GLOBAL enable_external_access=false) and an Arc SQL-layer denylist of the literal functions read_parquet( and arc_partition_agg(. The sandbox correctly blocks reads outside the storage root (OS files, http:// SSRF), but it allowlists the entire multi-tenant storage root, and the SQL denylist still misses the rest of the DuckDB I/O family the original advisory enumerated. As a result, any authenticated token — including one with permissions: [] — can read or enumerate every other database's data within the deployment, defeating RBAC.

Verified at HEAD c08b892 (v26.06.1-36, includes 26.06.2). Confirmed still live; no later commit blocks these functions.

Root cause (three composing gaps)

  1. Sandbox scope is the whole storage root, not the tenant. internal/database/sandbox.go:111 allowlists cfg.LocalStorageRoot (default storage.local_path=./data/arc). Every database lives under it — internal/storage/util.go:33 GetStoragePath => <base>/<database>/<measurement>/**/*.parquet. Any in-root path is sandbox-permitted regardless of which database owns it.
  2. RBAC never inspects table-valued functions. checkQueryPermissions (internal/api/query.go:1181) extracts table refs via extractTableReferences, which skips any name followed by ( as a function (internal/api/query.go:1116); with zero refs it returns allow (:1213-1216). So parquet_scan(...), glob(...), parquet_metadata(...), read_blob(...), read_csv_auto(...) are never authorization-checked.
  3. The SQL denylist matches only the literal spelling. userSQLReadParquetPattern (internal/api/query.go:2087) is \bread_parquet\s*\(. DuckDB's documented alias parquet_scan( is not matched, and read_csv_auto/read_json/read_text/read_blob/glob/parquet_metadata/parquet_schema are not blocked anywhere. The fix author documented this very threat at internal/api/query.go:2061-2064 (“a user scoped to db1.m1 could read /data/db2/secrets/**/*.parquet”) but blocked only the literal read_parquet(.

Exploit (authenticated, any token incl. permissions:[])

POST /api/v1/query
Authorization: Bearer <token>
Content-Type: application/json
{"sql": "SELECT * FROM parquet_scan('./data/arc/<victim-db>/<measurement>/**/*.parquet')"}
  • ValidateSQLRequest passes (parquet_scan( is not read_parquet(, not a dangerous keyword, single statement).
  • checkQueryPermissions extracts 0 table refs (it is a function call) and allows.
  • convertSQLToStoragePaths finds no read_parquet literal and no bare FROM measurement to rewrite, so the query passes through unchanged.
  • DuckDB runs parquet_scan('<root>/<victim-db>/...'); the path is inside allowed_directories, so the other database's rows are returned.

glob('./data/arc/**/*') enumerates every database/measurement/file; parquet_metadata(...)/parquet_schema(...) leak column names and per-column min/max value statistics; read_blob(...) returns raw file bytes. All operate within the allowlisted root.

Proof (poc/poc_sandbox_alias_bypass.py, poc/OUTPUT.txt)

Reproduces Arc's exact sandbox config (allowed_directories=[<root>], enable_external_access=false) on DuckDB 1.5.x (Arc pins duckdb-go-bindings 1.5.1) with a victim parquet written under the root:

[BLOCKED ] read_blob OUTSIDE root            -> Permission Error: Cannot access file ...
[EXECUTES] parquet_scan victim (read_parquet ALIAS) -> [('tenant-VICTIM-row', 42)]
[EXECUTES] glob enumerate every db under root -> [('.../data/arc/victim-db/secrets/f.parquet',)]
[EXECUTES] parquet_metadata victim           -> [('leaked','tenant-VICTIM-row','tenant-VICTIM-row'), ('v','42','42')]

The sandbox blocks escape (as designed) but permits every in-root cross-database read.

Impact

Cross-database / cross-tenant confidential read by any authenticated principal, including a least-privileged (permissions: []) token, on RBAC-enabled deployments. This is the same RBAC-bypass impact the original advisory described for read_parquet, still reachable via the alias and the unblocked I/O family. (Arc's SQLite token DB ./data/arc.db is a sibling of ./data/arc/ and is not in the allowlist, so it is not readable this way — the exposure is the time-series data of every database, plus its schema and value-range statistics.)

Remediation

Per-request scope the sandbox to the requesting token's authorized database path(s): allowed_directories = ['<base>/<authorized-db>/'], so the file layer cannot cross the RBAC boundary regardless of which function is used. Alternatively, replace the function-name denylist with a strict allowlist of permitted table functions (alias-resolved, reject all others). A denylist of literal spellings is structurally fragile — this is the second alias/family miss after the original audit.

Timeline

Published
4 hours ago
November 04, 2026 at 05:02 PM UTC
Fixed (v0.0.0-20260610131716-5dc8f53b70a3)
4 months ago
June 10, 2026 at 01:17 PM UTC
Last Modified
3 hours ago
November 04, 2026 at 05:15 PM UTC