GHSA-93cm-2v4m-c56c

    Dashboard / Vulnerabilities / GHSA-93cm-2v4m-c56c

    GHSA-93cm-2v4m-c56c

    Published: 4 Nov 2026Last Modified: 4 Nov 2026

    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.

    Affected packages

    Package

    Name: github.com/basekick-labs/arc

    Purl: pkg:golang/github.com/basekick-labs/arc

    Affected ranges

    Type: SEMVER

    Events:

    Introduced- 0
    Fixed -0.0.0-20260610131716-5dc8f53b70a3

    Affected versions

    Common Vulnerability Scoring System

    Attack Vector
    Network
    Adjacent
    Local
    Physical
    Privileges Required
    None
    Low
    High
    User Interaction
    None
    Required
    Scope
    Unchanged
    Changed
    Confidentiality
    None
    Low
    High
    Integrity
    None
    Low
    High
    Availability
    None
    Low
    High