Skip to content

fix(access): custom SQL cannot write; batches stop at the first failure - #526

Merged
huyplb merged 1 commit into
mainfrom
fix/access-utilities-review
Oct 12, 2026
Merged

huyplb merged 1 commit into
mainfrom
fix/access-utilities-review

Conversation

@huyplb

@huyplb huyplb commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

What and why

These are the fixes from a review of the Access and Utilities features.

1. Index Management custom SQL could write (security)

POST /schema/index-fragmentation runs caller-supplied SQL with utility.access only, so it never goes through the /sql/execute permission check. isSafeIndexFragmentationCustomSql stripped comments with a regex before checking for writes, and that regex didn't know a /* inside a string isn't a comment. Two statements passed the check because of this:

  • SELECT '/*', 1 INTO fox_probe FROM t -- */ creates a table on Postgres and SQL Server.
  • The same shape with INTO OUTFILE writes a file on a MySQL server.

The default roles only give utility.access to editors and above, who can already write, so this wasn't an escalation by default. It was one for any customized role matrix, and it skipped Safe Mode either way.

The check now takes the dialect and reads the text the same way the /sql/execute check does:

  • one statement, counted by sqlStatementCount
  • a SELECT or WITH, found by firstKeyword
  • every entry from sqlStatementCategories is read

The old check also rejected its own built-in templates for Postgres, CockroachDB, YugabyteDB and Redshift, because they contain a ; inside a comment. Those work now, and every dialect's template is tested against the check.

2. Clone Table and GRANT scripts now stop at the first failure

/sql/execute runs every statement and reports each one, which is what the editor wants. Clone Table assumed it stopped at the first failure. A failed index rename on the archive still ran the CREATE for the new live table. The error then said "stopped after N", and the advised "rename the archive back" failed because the live name was taken.

  • New stopOnError option on /sql/execute: it's off by default, so the editor's behaviour is unchanged.
  • Who uses it: Clone Table and runAccessSql (GRANT/REVOKE in Utilities → Database Access).
  • Better GRANT errors: a failed script now reads, for example, "Statement 2 of 3 failed: … The 1 before it was applied. The 1 after it did not run."

Smaller changes

  • ClickHouse backup names: ClickHouse treats \ as an escape inside a string, so disk and file names now double backslashes. A backup picked from system.backups is unescaped before it's quoted again.
  • Table-insight route: /schema/table-insight feeds the SQL Editor's Data Peek. It moved from the access routes to the schema routes, which own probeTableInsight. The path and permission are the same; it now has its own 30-per-minute limit instead of sharing the DBA utility one.
  • utility.index.drop: its description now says it only controls the Index Management button. Anyone with Change schema can still run DROP INDEX in the SQL Editor.

Not in this PR

  • One Database Access screen instead of two: this is a UI decision.
  • Removing the legacy editor.sidebar.utilities permission: saved role settings still name it.
  • Dialect-aware splitting in runAccessSql: splitSqlStatements has no dialect option. The server check already covers every reading, so this only affects correctness, not security.

Verification

  • Full npx vitest run: 505 files passed, 6,210 tests passed.
  • cd apps/web && npx tsc --noEmit is clean.
  • A/B check: every new test fails against the old code:
    • Custom SQL: the bypass, the dialect-specific statement split, the templates, and the server route refusing before the database is reached.
    • stopOnError: loop, route and runAccessSql, 5 breaks, each caught.
    • ClickHouse: quoting and reading names back.

🤖 Generated with Claude Code


Note

High Risk
Changes authentication-adjacent custom SQL validation and multi-statement execution semantics for utilities that run DDL/GRANT sequences; incorrect behavior could allow writes or leave databases half-migrated.

Overview
Security: Index Management custom probe SQL is validated with the same dialect-aware tokenizer as /sql/execute, replacing comment-stripping regexes that let SELECT … INTO and similar writes slip through on utility.access only.

Execution: /sql/execute gains optional stopOnError (default off). Clone Table and Database Access runAccessSql use it so ordered scripts halt after the first failure; GRANT/REVOKE errors now name the failing statement and what ran vs. skipped.

Other: ClickHouse backup/restore quotes disk and file names with proper backslash escaping. /schema/table-insight moves from access to schema routes (same URL and editor.run gate, dedicated rate limit). utility.index.drop permission text clarifies it only hides the Index Management drop button.

Reviewed by Cursor Bugbot for commit 073c56a. Bugbot is set up for automated code reviews on this repo. Configure here.

Index Management's custom fragmentation probe runs caller SQL under
utility.access, outside the /sql/execute permission gate. Its safety check
stripped comments with a regex before testing for writes, and the regex
did not know a `/*` inside a string is not a comment:
`SELECT '/*', 1 INTO t FROM x -- */` hid its INTO, passed, and created a
table on Postgres and SQL Server (MySQL `INTO OUTFILE` wrote a file). The
check now takes the dialect and reads the text as the gate does: one
statement by sqlStatementCount, a SELECT/WITH by firstKeyword, and every
sqlStatementCategories entry `read`. The old check also refused its own
Postgres, CockroachDB, YugabyteDB and Redshift templates (a `;` in a
comment); every dialect's template is now tested against it.

/sql/execute runs every statement and reports each, which the editor
wants. Clone Table assumed it stopped: a failed archive rename still ran
the new table's CREATE, so "stopped after N" was false and the advised
"rename the archive back" failed on the taken name. The endpoint takes
`stopOnError`; Clone Table and runAccessSql send it, and runAccessSql
names the failed statement and what ran before and did not run after it.

Also:
- ClickHouse backup literals double backslashes, and a picked
  system.backups name is unescaped before being quoted again
- /schema/table-insight moves to the schema routes that own it (its own
  30/min limiter; it shared the DBA utility one)
- utility.index.drop says it only governs the Index Management button

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cursor

cursor Bot commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: bb7e2317-4c2c-40df-9942-14f1502fae01)

@huyplb
huyplb merged commit 3942562 into main Oct 12, 2026
10 of 12 checks passed
@huyplb
huyplb deleted the fix/access-utilities-review branch October 12, 2026 16:45
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