[codex] Harden tenant isolation SQL boundaries#4
Draft
kopy wants to merge 2 commits intolukaskratzel:mainfrom
Draft
[codex] Harden tenant isolation SQL boundaries#4kopy wants to merge 2 commits intolukaskratzel:mainfrom
kopy wants to merge 2 commits intolukaskratzel:mainfrom
Conversation
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.
Summary
This PR hardens tenant/project isolation for agent-generated SQL compiled by
voight, especially whentenantScopingPolicyprotects tables by scope columns such asproject_id,tenant_id, orworkspace_id.Security assessment after the final pass:
The fixes in this PR still matter because tenant isolation often fails at exactly these catalog, policy, and database-runtime boundaries.
Findings Split
Agent-SQL-only Candidate
Direct physical-table bypass behind catalog aliases
If the intended security model is "the app exposes and protects the logical table name," this is the strongest finding.
Prerequisites:
public_eventsto a physical table such astracking.events.tables: ["public_events"].Example before the fix:
What was wrong:
The parser accepted valid SQL and the old policy matched only the logical alias, so direct physical access could compile without the injected
project_id = 'project-alpha'guard.Reassessment:
7.1 High, conditionalCVSS-style triage label.Fix:
The policy now resolves configured scope table names through the catalog, so protecting a logical alias also protects the resolved physical table identity.
Policy-identity Hardening
CTE reference alias shadowing
A CTE could be referenced with an alias that matched a scoped table name without tripping the shadowing guard.
Prerequisites:
events.WITHquery.Example before the fix:
Reassessment:
5.3 MediumCVSS-style triage label.Fix:
The policy now rejects CTE references whose CTE name or table alias shadows a scoped table identity.
Config / Runtime Hazards
MySQL string
project_idtype-coercion leakThis is not an Agent-SQL-only break. It requires trusted application code to pass the wrong policy context type.
Prerequisites:
project_idis a string column, for exampleVARCHAR.policyContext.projectId = 0orfalse.Example emitted guard before the fix:
What was wrong:
On MySQL 8.4, nonnumeric strings such as
project-alphaandproject-bravocompare equal to0, so a wrong trusted context type could widen the tenant boundary.Reassessment:
4.8 Medium, config/trusted-input hazardCVSS-style triage label.Fix:
tenantScopingPolicynow defaults scope values to strings. Numeric, bigint, and boolean scope columns must opt in throughscopeValueType, for examplescopeValueType: "bigint"for aBIGINT project_id.MySQL string collation widening
This is a schema/runtime hazard, not a compiler/parser bug.
Prerequisites:
project_iduses case/accent-insensitive collation, for exampleutf8mb4_0900_ai_ci.Example:
Under an unsafe collation, that can also match
PROJECT-ALPHA.Reassessment:
4.2 Medium, schema hazardCVSS-style triage label.Additional Hardening
Function-call sandbox note
The focused RCE pass flagged arbitrary function calls when
compile(...)is used withoutallowedFunctionsPolicy(...). This is not a default RDS MySQL 8.4 RCE:sys_exec/sys_evalare not RDS features and are not built-in MySQL 8.4 functions. They require dangerous third-party loadable UDFs or stored routines to be installed and executable. Built-ins such asLOAD_FILE()also require server privileges such asFILEand compatible server settings.Still, because
voightis intended for untrusted Agent SQL, callers should configure a strict function allowlist instead of relying on database deployment defaults.maxLimitPolicynow traverses every bound select, not only the outer select, and recursivedefaultLimitinsertion covers CTEs, derived tables, scalar subqueries,EXISTS, andINsubqueries.CASTtarget type identifiers are rejected unless they are simple unquoted raw SQL identifiers.GROUP BY,HAVING, andORDER BYnow preserves non-associative binary expression grouping.MySQL 8.4 / RDS Coverage
Added an opt-in live MySQL 8.4 integration test, gated by
VOIGHT_MYSQL84_URLorVOIGHT_MYSQL84_PORT. The live check ran against a disposablemysql:8.4container and covers:OR 1=1with direct victim predicatesLEFT JOINpredicatesEXISTS, and correlated aggregate subqueries0/falsecontext values for stringproject_idBIGINT project_idscoping viascopeValueType: "bigint"PROJECT-ALPHAforproject-alphaThe compile hardening suite also pins newer MySQL 8.4 features as rejected until the compiler models them and can tenant-scope every table-producing branch:
JSON_TABLETABLEVALUESUNION,INTERSECT,EXCEPTValidation
Ran:
Result:
packages/voight: 39 test files passed, 1 skipped, 337 tests passed, 6 skippedpackages/voight-parser: 1 test file passed, 10 tests passedAlso ran live MySQL 8.4 validation:
VOIGHT_MYSQL84_PORT=32768 corepack pnpm --filter @voight8/voight exec vitest run tests/integration/security/mysql84-project-isolation.test.tsResult: 1 test file passed, 6 tests passed.