Repository navigation
fix(cubejs): restore DuckDB, SQLite, JDBC, Databricks and CubeStore datasources, confined - #103
Merged
Merged
Conversation
…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]>
…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]>
This was referenced Oct 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
#102 refused the
duckdb,sqlite,jdbc,databricks-jdbcandcubestoretypes 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
:memory:, MotherDuck, files in the datasource's own directoryCUBEJS_LOCAL_DB_ROOT/<datasource id>/(read and write, with spill), remote prefixes listed by the operator inCUBEJS_DB_DUCKDB_ALLOWED_PATHS/proc/self/environis still readable); the S3 credential chain (the pod's own AWS credentials):memory:. It loads again, because sqlite3 is built from sourceATTACH. It is read-only, so rollups are built into Cube StoreCUBEJS_JDBC_DRIVERS_DIR, one jar per entrynode_modules; in-process URLs (checked against the exact network forms)jdbc:databricks:///jdbc:spark://URLsws/wss(http/httpsare rewritten to them); plain connections are pinned to the address that was checkedCUBEJS_CUBESTORE_DATASOURCE_ALLOW_PRIVATE=true(on-prem, which then needs a NetworkPolicy)Row rules
index.js). Aliases, and plain joins after an alias, get the joined cube's rules:orders.customer.events.eventgetsorders.customer.events.partition.Verification
yarn test: 743/743.ATTACH … (TYPE sqlite), writes to/tmpor/app,LOAD,../traversal and unlocking are all refused.ATTACH.queryRewritewith the real resolver on a compiled model: alias-then-join, member expressions and the blocked sentinel.http://: pinned;ws+unix://: refused./testok,get-schemaok, a SQLite path outside its directory refused.Round two (second adversarial review, all confirmed findings fixed, exploits rerun and closed)
httpfs,aws,motherduckandexcel; instances can load them but never download.s3_*settings are reset. Persistent secrets are off, so tenantCREATE SECRETworks.CALL enable_loggingandenable_profilingare refused, since the lock doesn't cover them.PatchMeasurewhose filters reach a ruled cube is ruled.yarn test748/748.Known pre-existing row-rule gaps (on
maintoo, not changed here)Rules are derived by re-reading model files, not from Cube's compiled model. As a result:
extendschildren don't inherit the parent's table;meta.source_tablecan steer table resolution;The fix is to build the rule map from Cube's compiled model (follow-up).
🤖 Generated with Claude Code