#21134·vitess

Bug Report: table ACL does not cover stored functions invoked inside expressions

Author: mattlordCreated Sep 16, 2026Updated Sep 18, 2026
LabelsType: BugComponent: Query ServingNeeds Triage

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.ROUTINES lookup at schema-reload time).

Flagging every statement containing a function call is not an option — it would deny almost every SELECT.

Reproduction Steps

  1. Start a keyspace with --queryserver-config-strict-table-acl and a table ACL that grants user u2 READER on a and nothing on secret.
  2. Directly in MySQL (not through Vitess), as vt_dba:
    sql
    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);
  3. As u2, through vtgate: select readsecret();
  4. Expected: Select command denied to user 'u2' ... (ACL check error). Actual: returns hidden.
  5. 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

bash
vttablet --version
Version: 25.0.0-SNAPSHOT (Git revision 1cf37324b4 branch 'main')

Operating System and Environment details

bash
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 a SET'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 TABLE source forms the grammar only partially accepts, which #21139 has to deny outright today
  • #21140 — still open: ALTER/REVERT VITESS_MIGRATION derive 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 after CALL
  • #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