必须完整回收create user权限及对应grant option,否则普通用户仍可建号;需严格匹配原授权结构执行revoke,排查角色、proxy及下游转授权限,并刷新权限缓存。

CREATE USER 权限必须被完整回收,否则普通用户仍能建号——光 revoke 权限但不处理 WITH GRANT OPTION,等于没做。
查清楚谁给了 CREATE USER 权限
普通用户能建号,说明它要么直接被授予了 CREATE USER,要么通过角色或 proxy 间接获得。不能只看 SHOW GRANTS FOR 'user'@'host' 就下结论:
- SHOW GRANTS 默认不展开角色权限,得加 USING 显式检查:SHOW GRANTS FOR 'baduser'@'%' USING 'role_name';
- 查 proxy 关系:SELECT * FROM mysql.proxies_priv WHERE User = 'baduser';
- 看是否被设为其他用户的 proxy(比如 SET ROLE 或 activate_all_roles_on_login 开启)
回收必须带原授权结构的 REVOKE
MySQL 不支持单独回收 GRANT OPTION,你得把当初授予权限的整条语句“反向执行”:
- 如果原始授权是:GRANT CREATE USER ON *.* TO 'baduser'@'%' WITH GRANT OPTION;
- 那就必须用:REVOKE CREATE USER ON *.* FROM 'baduser'@'%';
- 只写 REVOKE CREATE USER 不带 ON *.* 或错写成 ON mysql.*,权限不会被清掉
- REVOKE 后记得 FLUSH PRIVILEGES;(虽然 MySQL 8.0+ 多数情况自动刷新,但保险起见)
下游已转授的权限不会自动失效
这是最容易漏掉的一环:A 用户把 CREATE USER 授给了 B,你只 revoke A 的权限,B 的权限还在,且 B 自己可能也有 WITH GRANT OPTION。
- 必须手动排查所有已知被 A 授过权的账户:SELECT User,Host FROM mysql.user WHERE Create_user_priv = 'Y';
- 对每个结果执行:REVOKE CREATE USER ON *.* FROM 'user'@'host';
- 检查 mysql.role_edges(MySQL 8.0+)确认角色链里有没有残留路径
- 禁用 activate_all_roles_on_login 全局变量,避免角色在登录时静默激活
长期防御建议:默认不配 CREATE USER,改用专用运维账号
业务账号不该有建号能力,哪怕只临时给一次,也容易留下 GRANT OPTION 隐患。
- 所有新用户创建统一走 DBA 运维账号 + 审批流程
- 把 CREATE USER 权限只赋予 'dba'@'127.0.0.1' 这类受限账号
- 配合 ACCOUNT LOCK 和密码策略(如 PASSWORD EXPIRE INTERVAL 90 DAY),让高危权限账户本身也受控
- 注意:锁账号(ALTER USER ... ACCOUNT LOCK)不影响 proxy 或 DEFINER 执行,不是替代权限回收的方案











