最小权限原则要求创建用户后先验证权限为空,再按需授权;CREATE USER后必须立即SHOW GRANTS确认仅含USAGE,避免直接GRANT ALL导致权限失控。

MySQL 用户权限过大,本质是把数据库当成了“信任环境”而非“隔离边界”——误删库、误改表、SQL 注入横向扩散,几乎都源于一个账号拥有远超业务所需的权限。最小权限不是“减法练习”,而是从连接建立那一刻起,就用权限层做第一道访问控制。
CREATE USER 后不立刻 GRANT,先确认权限为空
新用户创建后,SHOW GRANTS FOR 'u'@'h' 返回空结果才是安全起点。很多团队跳过这步,直接 GRANT ALL ON *.*,以为“先通再调”,结果权限已写入 mysql.user 表,后续 REVOKE 也难清干净。
- 必须执行
CREATE USER 'app'@'10.20.30.%' IDENTIFIED BY 'pwd';后立刻SHOW GRANTS FOR 'app'@'10.20.30.%';,确认输出为GRANT USAGE ON *.* TO 'app'@'10.20.30.%'(USAGE是空权限,仅表示能连) - 不要用
GRANT USAGE当占位符再叠加授权——它不提供任何实际能力,反而掩盖真实权限状态 - 老版本 MySQL(REVOKE SELECT ON information_schema.* FROM 'u'@'h' 需要
SUPER权限才能执行,且不能撤回隐式继承的元数据访问权;8.0.29+ 更可靠的做法是先设show_compatibility_56=OFF,再配合REVOKE SELECT ON *.*,最后只授具体库表
用角色(ROLE)封装权限,避免直接授给用户
MySQL 8.0+ 的角色不是语法糖,是解决权限漂移的关键机制。直接 GRANT 给用户,权限会随时间发散;而通过角色统一管控,变更只需动一处。
- 建角色:
CREATE ROLE 'web_reader', 'web_writer'; - 授角色权限(注意粒度):
GRANT SELECT ON shop.products TO 'web_reader';,GRANT INSERT, UPDATE ON shop.orders TO 'web_writer';—— 不要GRANT SELECT ON shop.*,除非真需要读全部表 - 把角色给用户:
GRANT 'web_reader' TO 'webapp'@'10.20.30.%'; - 必须启用默认角色:
SET DEFAULT ROLE 'web_reader' TO 'webapp'@'10.20.30.%';,否则登录后权限不生效 -
SHOW GRANTS FOR 'webapp'@'10.20.30.%'默认不显示角色权限,要查真实效果得用SHOW GRANTS FOR 'webapp'@'10.20.30.%' USING 'web_reader';或查INFORMATION_SCHEMA.ROLE_TABLE_GRANTS
禁用 GRANT OPTION 和匿名用户,堵住权限扩散口
GRANT OPTION 是比 DROP DATABASE 更隐蔽的风险点——它允许用户把自己拥有的权限转授出去,形成不可控的权限链。一旦被利用,攻击者可自行创建高权限账号。
- 检查现有用户是否意外开启:
SELECT User,Host,Grant_priv FROM mysql.user WHERE Grant_priv='Y'; - 撤销方式不是
REVOKE GRANT OPTION,而是重授时不加WITH GRANT OPTION;若已授,需先REVOKE所有权限,再重新GRANT(不含WITH) - 匿名用户
''@'localhost'是历史遗留漏洞,常被绕过认证直连,务必运行DROP USER IF EXISTS ''@'localhost'; - 应用连接字符串里加
--skip-secure-auth完全无效,它只影响客户端认证协议协商,不改变权限粒度
权限变更后必须验证实际 SQL 行为,而非仅看 GRANT 输出
权限系统存在层级继承和 host/user 组合匹配逻辑,SHOW GRANTS 看起来干净,不代表运行时没越权。比如你对 'u'@'%' 授了 SELECT ON db.t1,又对 'u'@'localhost' 授了 SELECT ON db.*,那么 REVOKE SELECT ON db.t1 FROM 'u'@'%' 不会影响其对 db.t2 的访问——因为权限按完整 host/user 对独立存储。
- 验证前先清理测试环境:用目标用户连接,执行
USE information_schema; SELECT SCHEMA_NAME FROM SCHEMATA;,确认是否还能看到其他库 - 模拟应用行为:跑一条真实的业务 SQL(如
UPDATE orders SET status='shipped' WHERE id=123),再试一条越界操作(如DROP TABLE logs),观察报错是否为ERROR 1142 (42000): DROP command denied - ORM 或连接池初始化时可能尝试
SET SESSION sql_log_bin = 0,这需要BINLOG_ADMIN动态权限(8.0 新增),不是传统GRANT能覆盖的,得单独GRANT BINLOG_ADMIN ON *.*
最小权限落地最难的不是语法,而是打破“先给全再收”的惯性——每次加权限,都要能回答“这条 SQL 为什么非它不可”。生产环境里,一个没被日志记录的 ALTER TABLE,往往比一百次 SELECT 更危险。











