GHSA-93CM-2V4M-C56C
Vulnerability from github – Published: 2026-11-04 17:02 – Updated: 2026-11-04 17:02Summary
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)
- Sandbox scope is the whole storage root, not the tenant.
internal/database/sandbox.go:111allowlistscfg.LocalStorageRoot(defaultstorage.local_path=./data/arc). Every database lives under it —internal/storage/util.go:33GetStoragePath=><base>/<database>/<measurement>/**/*.parquet. Any in-root path is sandbox-permitted regardless of which database owns it. - RBAC never inspects table-valued functions.
checkQueryPermissions(internal/api/query.go:1181) extracts table refs viaextractTableReferences, which skips any name followed by(as a function (internal/api/query.go:1116); with zero refs it returns allow (:1213-1216). Soparquet_scan(...),glob(...),parquet_metadata(...),read_blob(...),read_csv_auto(...)are never authorization-checked. - The SQL denylist matches only the literal spelling.
userSQLReadParquetPattern(internal/api/query.go:2087) is\bread_parquet\s*\(. DuckDB's documented aliasparquet_scan(is not matched, andread_csv_auto/read_json/read_text/read_blob/glob/parquet_metadata/parquet_schemaare not blocked anywhere. The fix author documented this very threat atinternal/api/query.go:2061-2064(“a user scoped to db1.m1 could read /data/db2/secrets/**/*.parquet”) but blocked only the literalread_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')"}
ValidateSQLRequestpasses (parquet_scan(is notread_parquet(, not a dangerous keyword, single statement).checkQueryPermissionsextracts 0 table refs (it is a function call) and allows.convertSQLToStoragePathsfinds noread_parquetliteral and no bareFROM measurementto rewrite, so the query passes through unchanged.- DuckDB runs
parquet_scan('<root>/<victim-db>/...'); the path is insideallowed_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": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/basekick-labs/arc"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.0.0-20260610131716-5dc8f53b70a3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-55098"
],
"database_specific": {
"cwe_ids": [
"CWE-200",
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2026-11-04T17:02:37Z",
"nvd_published_at": "2026-10-30T20:17:09Z",
"severity": "MODERATE"
},
"details": "### Summary\n\nThe fix for CVE-2026-47735 (released 2026.06.1) is incomplete. It added a DuckDB sandbox (`SET GLOBAL allowed_directories=[\u003cstorage root\u003e]; 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 \u2014 including one with `permissions: []` \u2014 can read or enumerate **every other database\u0027s data** within the deployment, defeating RBAC.\n\nVerified at HEAD `c08b892` (`v26.06.1-36`, includes 26.06.2). Confirmed still live; no later commit blocks these functions.\n\n### Root cause (three composing gaps)\n\n1. **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 \u2014 `internal/storage/util.go:33` `GetStoragePath` =\u003e `\u003cbase\u003e/\u003cdatabase\u003e/\u003cmeasurement\u003e/**/*.parquet`. Any in-root path is sandbox-permitted regardless of which database owns it.\n2. **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.\n3. **The SQL denylist matches only the literal spelling.** `userSQLReadParquetPattern` (`internal/api/query.go:2087`) is `\\bread_parquet\\s*\\(`. DuckDB\u0027s 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` (\u201ca user scoped to db1.m1 could read /data/db2/secrets/**/*.parquet\u201d) but blocked only the literal `read_parquet(`.\n\n### Exploit (authenticated, any token incl. permissions:[])\n\n```\nPOST /api/v1/query\nAuthorization: Bearer \u003ctoken\u003e\nContent-Type: application/json\n{\"sql\": \"SELECT * FROM parquet_scan(\u0027./data/arc/\u003cvictim-db\u003e/\u003cmeasurement\u003e/**/*.parquet\u0027)\"}\n```\n\n- `ValidateSQLRequest` passes (`parquet_scan(` is not `read_parquet(`, not a dangerous keyword, single statement).\n- `checkQueryPermissions` extracts 0 table refs (it is a function call) and allows.\n- `convertSQLToStoragePaths` finds no `read_parquet` literal and no bare `FROM measurement` to rewrite, so the query passes through unchanged.\n- DuckDB runs `parquet_scan(\u0027\u003croot\u003e/\u003cvictim-db\u003e/...\u0027)`; the path is inside `allowed_directories`, so the other database\u0027s rows are returned.\n\n`glob(\u0027./data/arc/**/*\u0027)` 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.\n\n### Proof (poc/poc_sandbox_alias_bypass.py, poc/OUTPUT.txt)\n\nReproduces Arc\u0027s exact sandbox config (`allowed_directories=[\u003croot\u003e]`, `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:\n\n```\n[BLOCKED ] read_blob OUTSIDE root -\u003e Permission Error: Cannot access file ...\n[EXECUTES] parquet_scan victim (read_parquet ALIAS) -\u003e [(\u0027tenant-VICTIM-row\u0027, 42)]\n[EXECUTES] glob enumerate every db under root -\u003e [(\u0027.../data/arc/victim-db/secrets/f.parquet\u0027,)]\n[EXECUTES] parquet_metadata victim -\u003e [(\u0027leaked\u0027,\u0027tenant-VICTIM-row\u0027,\u0027tenant-VICTIM-row\u0027), (\u0027v\u0027,\u002742\u0027,\u002742\u0027)]\n```\n\nThe sandbox blocks escape (as designed) but permits every in-root cross-database read.\n\n### Impact\n\nCross-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\u0027s 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 \u2014 the exposure is the time-series data of every database, plus its schema and value-range statistics.)\n\n### Remediation\n\nPer-request scope the sandbox to the requesting token\u0027s authorized database path(s): `allowed_directories = [\u0027\u003cbase\u003e/\u003cauthorized-db\u003e/\u0027]`, 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 \u2014 this is the second alias/family miss after the original audit.",
"id": "GHSA-93cm-2v4m-c56c",
"modified": "2026-11-04T17:02:37Z",
"published": "2026-11-04T17:02:37Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Basekick-Labs/arc/security/advisories/GHSA-93cm-2v4m-c56c"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-55098"
},
{
"type": "WEB",
"url": "https://github.com/Basekick-Labs/arc/pull/495"
},
{
"type": "WEB",
"url": "https://github.com/Basekick-Labs/arc/commit/5dc8f53b70a3871b16b0cebf328f07c578af7a45"
},
{
"type": "PACKAGE",
"url": "https://github.com/Basekick-Labs/arc"
},
{
"type": "WEB",
"url": "https://github.com/Basekick-Labs/arc/tree/v26.06.2"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Arc: DuckDB I/O aliases bypass database RBAC after CVE-2026-47735"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.