Skip to content

fix(store): validate workspace names; restore provisioning for admin deploy - #43

Merged
maruyue merged 1 commit into
mainfrom
fix/non-admin-no-ddl
Oct 21, 2026
Merged

maruyue merged 1 commit into
mainfrom
fix/non-admin-no-ddl

Conversation

@maruyue

@maruyue maruyue commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #42

#42 merged the DDL gate for ordinary users. Two issues surfaced while auditing the
permission paths afterwards — one pre-existing, one a regression from that change.

1. Workspace name was interpolated into SQL without validation (pre-existing)

DorisStore builds table names as active_store_<workspace>, but only the
workspace-create route validated the name. A crafted workspace query/body field
on /mcp/web/models, /mcp/web/staging/list, /mcp/web/semantic/files, etc. was
concatenated into every store statement:

-- workspace = "x UNION SELECT 1,2,3 -- "
SELECT filename, updated_at, LENGTH(content) AS size_bytes FROM active_store_x UNION SELECT 1,2,3 --  ORDER BY filename

Read paths bypass validate_readonly (store SQL never goes through sqlglot), and
workspace/delete allowed name = "x, dw.users" → DROP TABLE IF EXISTS active_store_x, dw.users.

Fix: validate the name as a plain identifier in DorisStore.__init__ and
drop_workspace_tables, and ignore legacy tables with invalid names in discovery.

2. Example deploy lost DDL rights (regression from #42)

Deploy/delete run in a background task created from the admin-cookie route, which
never set the provisioning flag. On a fresh install seed() creates the database
but not workspace_metadata, so bump_semantic_version() failed with
"Unknown table". Mark the admin-only deploy path as allowed to provision.

Tests

test/test_readonly_storage.py now has 12 cases (invalid names rejected before any
connection, discovery filters them, admin deploy path can provision, plus the
original non-admin/no-DDL invariants). Full offline suite passes.

Not addressed here

The session-affinity proxy forwards to an IP taken from an unsigned cookie
(SSRF, restricted to /mcp/web* on the fixed MCP port). Fixing it needs an HMAC
secret shared across nodes or a node allow-list — separate decision, separate PR.

…deploy

Two issues found while auditing the permission paths:

1. DorisStore interpolates the workspace name into table names
   (active_store_<ws>), and only the workspace-create route validated it.
   A crafted "workspace" query/body field could therefore inject SQL into
   every store statement (e.g. `UNION SELECT ...` in reads, or a second
   table in DROP TABLE via workspace/delete). Validate the name as a plain
   identifier in DorisStore.__init__ and drop_workspace_tables, and skip
   legacy tables with invalid names during discovery.

2. Example deploy/delete run in a background task created from the
   admin-cookie route, which never set the provisioning flag. On a fresh
   install seed() creates the database but not workspace_metadata, so
   bump_semantic_version() failed with "Unknown table". Mark the
   admin-only deploy path as allowed to provision before seeding.
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