必须显式回收高危权限并用show grants验证,因revoke不报错也不保证生效,且角色继承或权限残留可能导致高危权限仍存在。

单纯靠参数化查询防不住注入后的提权——只要账号权限过大,攻击者拿到查询入口就能读系统表、写文件、调用UDF执行命令。必须显式回收高危权限,并验证是否真正生效。
为什么REVOKE FILE/PROCESS/SUPER后还要SHOW GRANTS验证
MySQL 的 REVOKE 命令是“尽力而为”:它不会报错,哪怕目标权限原本就不存在。比如对一个本来没 FILE 权限的账号执行 REVOKE FILE ON *.* FROM 'app_user'@'%',命令成功返回,但实际什么也没做。
- 必须紧接着执行
SHOW GRANTS FOR 'app_user'@'%',逐行确认输出里不再出现FILE、PROCESS、SUPER - 特别注意权限继承场景:如果该账号属于某个角色(role),而角色仍有这些权限,
SHOW GRANTS会显示角色授予的权限,需一并回收 - 生产环境建议写成巡检脚本,定期扫描所有应用账号的
GRANTS输出,grep 匹配FILE\|PROCESS\|SUPER
secure_file_priv设为空字符串或NULL时的隐患
secure_file_priv 是封堵 LOAD DATA INFILE 和 SELECT ... INTO OUTFILE 的关键闸门。设成空字符串('')或 NULL 看似“彻底禁用”,实则埋雷:
- 空字符串允许任意路径读写(MySQL 5.7+ 行为变更,部分版本下等效于未设)
-
NULL值会让LOAD DATA INFILE直接报错ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option so it cannot execute this statement,但旧应用可能因此崩溃,导致运维误判为配置错误而改回宽松值 - 正确做法是设为受限目录,如
/var/lib/mysql-files/,并确保该目录属主为mysql、权限为750,且不挂载在 Web 可访问路径下
如何确认高危UDF函数已被清除
攻击者常通过安装 lib_mysqludf_sys 等第三方 UDF 实现 sys_exec()、sys_eval(),绕过权限限制执行系统命令。不能只靠“没装就安全”:
- 运行
SELECT * FROM mysql.func WHERE name LIKE '%sys%';,若返回任何记录,立即执行DROP FUNCTION sys_exec;(或对应函数名) - 检查
SELECT plugin_name, plugin_status FROM information_schema.plugins WHERE plugin_name LIKE '%sys%';,禁用相关插件(需 SUPER 权限) - 对业务账号显式拒绝函数执行权限:
REVOKE EXECUTE ON FUNCTION mysql.sys_exec FROM 'app_user'@'%';,否则即使函数存在,该账号也无法调用 - 更彻底的做法:启动 mysqld 时加
--skip-secure-auth并禁用动态加载(--plugin-load-add=留空),但需权衡兼容性
为什么最小权限要细化到数据库级别而非全局
给账号授 GRANT SELECT ON *.* 看似方便,实则等于开放全部库的元数据——攻击者注入成功后可直接查 mysql.user 拿哈希,查 information_schema.TABLES 扫库,甚至用 SELECT LOAD_FILE('/etc/my.cnf') 泄露配置。
- 必须限定到具体库:
GRANT SELECT, INSERT ON finance_db.invoice TO 'app_user'@'%'; - 避免跨库操作:不授
ON *.*或ON mysql.*,连performance_schema都不该开(含敏感性能数据) - 测试账号尤其危险:默认
test库常被忽略,务必执行DROP DATABASE IF EXISTS test;并删除user表中Host='%' AND User=''的匿名账户
最易被忽略的一点:权限回收不是一次性动作。每次新建数据库、添加新模块、或迁移账号到新实例时,都得重新跑一遍 SHOW GRANTS + REVOKE + GRANT 流程——自动化的权限审计比人工复查可靠得多。











