Skip to content

fix(cubejs): restore DuckDB, SQLite, JDBC, Databricks and CubeStore datasources, confined - #103

Merged
acmeguy merged 4 commits into
mainfrom
fix/restore-db-types
Oct 31, 2026
Merged

acmeguy merged 4 commits into
mainfrom
fix/restore-db-types

Conversation

@acmeguy

@acmeguy acmeguy commented Oct 31, 2026 •

Copy link
Copy Markdown

Why

#102 refused the duckdb, sqlite, jdbc, databricks-jdbc and cubestore types for tenant datasources. Database support must never disappear. This restores every type Cube knows and removes only the settings that reach the cube pod or other tenants. Join aliases (new in Cube 1.8) work under row rules instead of being refused.

An adversarial review of the first version found bypasses. All of them are fixed in the second commit, and the reviewers' exploits were rerun against the fixed image and are closed. A second review of the new design is running.

What each type keeps, and what it loses

Type Works Taken away
DuckDB :memory:, MotherDuck, files in the datasource's own directory CUBEJS_LOCAL_DB_ROOT/<datasource id>/ (read and write, with spill), remote prefixes listed by the operator in CUBEJS_DB_DUCKDB_ALLOWED_PATHS Every other path, at creation (no extension auto-install or auto-load) and before any tenant SQL (allowed directories, external access off, config locked; the driver refuses to start if /proc/self/environ is still readable); the S3 credential chain (the pod's own AWS credentials)
SQLite Files in the datasource's own directory, :memory:. It loads again, because sqlite3 is built from source Other paths, and ATTACH. It is read-only, so rollups are built into Cube Store
Generic JDBC Network JDBC URLs; driver jars from CUBEJS_JDBC_DRIVERS_DIR, one jar per entry The Derby/HSQLDB jars in node_modules; in-process URLs (checked against the exact network forms)
Databricks jdbc:databricks:// / jdbc:spark:// URLs Driver logging settings in the URL (they write files and trace other tenants' connections)
CubeStore An external store reached over ws/wss (http/https are rewritten to them); plain connections are pinned to the address that was checked Our own CubeStore, by name or address, including IPv4-mapped forms; private networks, NAT64 and 6to4 addresses, unless CUBEJS_CUBESTORE_DATASOURCE_ALLOW_PRIVATE=true (on-prem, which then needs a NetworkPolicy)

Row rules

  • Member paths resolve with Cube's own resolver on the model compiled for the request (index.js). Aliases, and plain joins after an alias, get the joined cube's rules: orders.customer.events.event gets orders.customer.events.partition.
  • SQL API member expressions get rules for the cubes they read; while rules apply, an expression shape it can't read is refused.
  • A blocked query with only time dimensions still gets its sentinel; with nothing to attach the sentinel to, the query is refused.

Verification

  • Image yarn test: 743/743.
  • Behaviour tests against the installed, patched drivers:
    • DuckDB writes and spills inside its own directory; environ reads, ATTACH … (TYPE sqlite), writes to /tmp or /app, LOAD, ../ traversal and unlocking are all refused.
    • SQLite is read-only and refuses ATTACH.
  • End-to-end queryRewrite with the real resolver on a compiled model: alias-then-join, member expressions and the blocked sentinel.
  • Reviewers' exploits rerun against the fixed image:
    • SQL API member expression: the other tenant's row is no longer returned;
    • alias then plain join: filtered;
    • CubeStore DNS rebinding over http://: pinned;
    • mapped IPv6 and ws+unix://: refused.
  • Earlier on this PR, end to end: DuckDB and SQLite datasources on a local 1.8.16 server: /test ok, get-schema ok, a SQLite path outside its directory refused.

Round two (second adversarial review, all confirmed findings fixed, exploits rerun and closed)

  • DuckDB extensions: operator extensions must be ones whose file and network access goes through DuckDB's own checks. The sqlite, postgres and mysql scanners don't, so with them configured the datasource refuses to start. Loaded extensions are checked before the lock, and nothing can load after it. The image ships httpfs, aws, motherduck and excel; instances can load them but never download.
  • DuckDB secrets: operator S3 credentials become a redacted DuckDB secret, and the readable s3_* settings are reset. Persistent secrets are off, so tenant CREATE SECRET works.
  • DuckDB logging: CALL enable_logging and enable_profiling are refused, since the lock doesn't cover them.
  • MotherDuck: it opens again; a path without a token is refused instead of blocking on an interactive login.
  • SQLite:
    • an unopenable file fails its queries instead of crashing the pod;
    • per-datasource directories are created;
    • in-source pre-aggregations get a clear "must be external" error;
    • the pragmas that change the temp and data directories are refused.
  • Row rules: a PatchMeasure whose filters reach a ruled cube is ruled.
  • CubeStore: the port must be numeric (it could carry a host through userinfo); host and port become a pinned URL.
  • JDBC:
    • driver logging settings are refused for every JDBC-family type, in the URL or the properties;
    • generic JDBC and Databricks share one complete JVM class path, so either works whichever connects first.
  • Tests: image yarn test 748/748.

Known pre-existing row-rule gaps (on main too, not changed here)

Rules are derived by re-reading model files, not from Cube's compiled model. As a result:

  • views get no rules;
  • JS models the parser can't read are skipped silently;
  • extends children don't inherit the parent's table;
  • member SQL that references joined cubes isn't ruled for those cubes;
  • meta.source_table can steer table resolution;
  • YAML merge keys are read differently from Cube.

The fix is to build the rule map from Cube's compiled model (follow-up).

🤖 Generated with Claude Code

…atasources, confined

#102 refused these types for tenants. Database support never disappears:
every type Cube knows is available again, and only the settings that reach
this pod or other tenants are taken away.

- DuckDB: the driver is patched to lock itself down before any tenant SQL
  (initSql included) runs: local filesystem off, no extension installs,
  configuration locked; it refuses to start if local files stay reachable.
  Remote data (S3/HTTP through operator-loaded extensions, MotherDuck) and
  :memory: work. Local database files only under CUBEJS_LOCAL_DB_ROOT. The
  S3 credential chain (this pod's AWS credentials) is dropped from tenant
  params.
- SQLite: builds from source again (the prebuilt sqlite3 6 needs glibc
  2.38), database files only under CUBEJS_LOCAL_DB_ROOT, and the driver is
  patched to allow no ATTACHed databases.
- JDBC: driver jars only from CUBEJS_JDBC_DRIVERS_DIR, never the Derby or
  HSQLDB jars in node_modules; no in-process database URLs.
- Databricks: jdbc:databricks:// URLs (the driver already pins its class
  and class path).
- CubeStore: an explicit host that is neither ours nor on a private network
  (CUBEJS_CUBESTORE_DATASOURCE_ALLOW_PRIVATE=true for on-prem), pinned to
  the address checked.
- Join aliases: row rules now follow them instead of refusing the query. A
  path like orders.customer.city gets the users table's rules as
  orders.customer.<dimension>; pruning and the access list work per
  instance; extends children inherit aliases; an alias the map cannot
  resolve is still refused.

Tests: confinement rules; DuckDB and SQLite lockdown proven against the
installed, patched drivers; a rule filter on an alias path lands on the
aliased table in Tesseract's SQL. 740/740 in the image.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
acmeguy and others added 3 commits October 31, 2026 11:20
…nfinement

DuckDB
- The post-hoc LocalFileSystem lock was bypassable: a sqlite-format database
  file pulled in the sqlite extension at creation, whose file access the lock
  didn't cover (cross-tenant read, writes anywhere). It also broke writes to
  the datasource's own file and spilling. Now: no extension auto-install or
  auto-load and a private spill directory at creation; before any tenant SQL,
  file access only inside the datasource's own directory (plus operator
  remote prefixes, CUBEJS_DB_DUCKDB_ALLOWED_PATHS), external access off,
  configuration locked, and it refuses to start if /proc/self/environ is
  still readable.
- Local database files live per datasource: <root>/<datasource id>/.

SQLite: read-only, so Cube builds rollups into Cube Store and never needs the
ATTACH that the attach limit refuses.

Row rules
- Member paths resolve with Cube's own resolver on the model compiled for the
  request (index.js), so an alias followed by a plain join is ruled too.
- SQL API member expressions rule their cubes; a shape row rules can't read
  is refused while rules apply (it used to pass through).
- A blocked query with only time dimensions gets its sentinel; with nothing
  to attach it to, it is refused.

CubeStore: websocket URLs only (http/https rewritten), every plain
connection pinned, IPv4-mapped/NAT64/6to4 addresses normalised or treated as
private.

JDBC: one jar per class-path entry (a separator smuggled others in);
case-exact network URLs for Derby/HSQLDB/H2. Databricks: no driver logging
settings in the URL (they write files and trace every tenant's connections).

The reviewers' exploits were rerun against the fixed image: all closed.
743/743 in the image.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
DuckDB
- Operator extensions (CUBEJS_DB_DUCKDB_EXTENSIONS / community) must be ones
  whose file and network access goes through DuckDB's own checks: sqlite,
  postgres, mysql scanners read files and networks around the sandbox, so a
  datasource refuses to start with them. What is loaded is checked before
  the lock; nothing can load after it.
- The image ships httpfs, aws, motherduck and excel; instances may load but
  never download extensions. MotherDuck opens again (it needs its extension
  at creation); a MotherDuck path without a token is refused instead of
  blocking on an interactive login.
- Operator S3 credentials become a DuckDB secret (redacted) and the readable
  s3_* settings are reset; persistent secrets are off, so tenant CREATE
  SECRET works and nothing is shared through ~/.duckdb.
- CALL enable_logging/enable_profiling (not covered by the lock) refused.

SQLite: an unopenable file fails its queries instead of crashing the process
(open errors were an unhandled 'error' event); per-datasource directories
are created; in-source pre-aggregations get a clear "must be external"
error; the pragmas that move SQLite's process-wide temp/data directories are
refused.

Row rules: a PatchMeasure that reaches a ruled cube only through its filters
(normalised [...cubes, sql] form) is ruled.

CubeStore: the port must be numeric (it could carry a host via userinfo);
host/port become a pinned URL.

JDBC: driver logging settings refused for every JDBC-family type, in the URL
or the properties (the Databricks driver class could be loaded through the
generic type). Generic JDBC and Databricks share one complete JVM class
path, so either works whichever connected first.

The second review's exploits were rerun against the fixed image: closed.
748/748 in the image.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Row rules: cubes the SQL API names only in a subquery join's ON or in
  join hints are ruled (a subquery join could read another tenant's rows).
- DuckDB: a remote prefix (CUBEJS_DB_DUCKDB_ALLOWED_PATHS=s3://...) is only
  served with CUBEJS_DB_DUCKDB_EGRESS_RESTRICTED=true: DuckDB checks the
  path, not the host a tenant secret's ENDPOINT or a redirect sends it to,
  so the cube pods' egress must be restricted (NetworkPolicy) first.
  Logging/profiling calls in initSql are refused too; SQL built at run time
  can still reach them (documented).
- SQLite: the process-wide heap-limit pragmas are refused, in PRAGMA and
  table-valued form (one datasource could starve every other's memory).

The third review's exploits were rerun: closed. 750/750 in the image.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@acmeguy
acmeguy merged commit 8086e07 into main Oct 31, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant