Bug Report: table ACL does not cover stored functions invoked inside expressions
Overview of the Issue
Under strict table ACL, vttablet checks only the tables a statement itself names. A stored function invoked inside an expression is not a table, so nothing is derived for it — yet its body runs with vttablet's own MySQL privileges and can read (or, if the body contains DML, write) any table in the keyspace. A caller who holds no grants on secret can read it through any function whose body does.
select readsecret() Permissions=[] TablesUndetermined=false
select * from a where readsecret() = 1 Permissions=[{a READER}] TablesUndetermined=false
update a set x = readsecret() where ... Permissions=[{a WRITER}] TablesUndetermined=false(planbuilder.BuildPermissions at main 1cf37324b4 with #21053 applied.) The first statement passes checkPermissions with an empty loop; the other two check a only. Whatever readsecret()'s body touches is never checked.
This is the same opaque-body problem as CALL, which #21053 (GHSA-w6mx-2f8x-pqf4) closes by denying the statement for non-exempt callers. A function is not CALLed — it is reached through an expression — so that gate never fires. #21053 records this as a known gap in its deployment notes; this issue tracks it.
What bounds it: Vitess's parser rejects CREATE FUNCTION (syntax error at vtgate and vttablet), so the function must have been created with direct MySQL access. The exposure is that any non-exempt caller can invoke any operator-defined stored function under strict table ACL, with its body's table access unchecked.
Closing it needs one of:
- A routine catalog on the tablet, so the planner can tell a stored function from a built-in and either derive its body's tables or flag the statement
TablesUndetermined. - A policy: under strict table ACL, deny statements that invoke a stored function for non-exempt callers (built-ins must still pass, so this needs the catalog too, or a
mysql.func/information_schema.ROUTINESlookup at schema-reload time).
Flagging every statement containing a function call is not an option — it would deny almost every SELECT.
Reproduction Steps
- Start a keyspace with
--queryserver-config-strict-table-acland a table ACL that grants useru2READER onaand nothing onsecret. - Directly in MySQL (not through Vitess), as
vt_dba:create table secret (id int primary key, v varchar(64)); insert into secret values (1, 'hidden'); create function readsecret() returns varchar(64) reads sql data return (select v from secret where id = 1); - As
u2, through vtgate:select readsecret(); - Expected:
Select command denied to user 'u2' ... (ACL check error). Actual: returnshidden. - Confirm the direct path is denied for the same caller:
select v from secret where id = 1;→Select command denied ... for table 'secret'.
Binary Version
vttablet --version
Version: 25.0.0-SNAPSHOT (Git revision 1cf37324b4 branch 'main')Operating System and Environment details
Any. Verified against the planner on macOS arm64 with MySQL 8.0.46.Log Fragments
(none: the statement is not logged as denied because no permission is derived for it)Related
This is one of a set of changes that came out of one investigation into what a stored procedure, or any statement whose tables the planner never sees, can do on a vttablet. They reference each other so the whole picture is reachable from any one of them.
Table ACL — GHSA-w6mx-2f8x-pqf4
- #21053 — fail closed under strict table ACL for statements whose tables the parser discards (
DO,CALL,REPAIR,OPTIMIZE,LOAD DATA) - #21139 — stacked on #21053: check the reads embedded in statements the planner does parse (
CREATE TABLE ... AS SELECT,EXPLAIN ANALYZE,SHOW ... WHERE,SET), reject connection settings that carry a subquery, and have a targeted vtgate session store aSET's value rather than its expression - #21134 — (this issue) still open: a stored function invoked inside an expression runs its body outside the table ACL
- #21138 — still open: parse the
CREATE TABLEsource forms the grammar only partially accepts, which #21139 has to deny outright today - #21140 — still open:
ALTER/REVERT VITESS_MIGRATIONderive no permissions, so any authenticated caller can control Online DDL migrations; split out of #21053's review
Stored-procedure session residue
- #21046 — procedure-body session state (
SET SESSION, temp tables) persists on pooled connections afterCALL - #21062 — fixes #21046 by discarding the pooled connection after
CALL - #21063 — still open: the same residue on a transaction connection recycled at commit
- #21066 — still open: the pooled-connection baseline differs between fresh and settings-reset connections on servers using
init_connect
Source: vitessio/vitess