“%”是权限失控开关而非方便,因其表示任意主机且绕过网络校验,易致公网爆破;必须删除残留@'%'账号、显式指定host建号、转义含_或%的库名以保障安全。

为什么'%'不是“方便”,而是权限失控开关
MySQL里'%'在host字段中明确表示“任意主机”,不区分内外网、不校验IP合法性、不走防火墙白名单逻辑。只要数据库监听0.0.0.0:3306且未启用skip-networking,这个账号就能从公网被爆破或扫描到。这不是配置疏漏,是语义上主动开放——哪怕你只开了一个app_user@'%',攻击者连上后就能执行DROP DATABASE、读取mysql.user哈希,甚至通过LOAD DATA LOCAL INFILE读取服务器文件。
删掉'%'账号前必须查清所有同名用户
MySQL按User+Host二元组匹配,'app_user'@'192.168.1.100'和'app_user'@'%'共存时,后者优先级更低但依然生效。不先清理,新账号永远被绕过。
- 执行
SELECT User, Host FROM mysql.user WHERE User = 'app_user';,确认是否存在'%'记录 - 若存在,用
DROP USER 'app_user'@'%';彻底删除(不是REVOKE) - 注意:不能只删
mysql.user表行,必须用DROP USER,否则权限缓存残留 - 删完立刻
FLUSH PRIVILEGES;,否则旧连接仍可能维持权限
新建账号必须显式指定host,禁止省略
CREATE USER不写@'host',MySQL默认补@'%'。这是最隐蔽的越权入口,尤其在脚本批量建号时极易中招。
- 正确写法:
CREATE USER 'app_user'@'192.168.50.42' IDENTIFIED BY 'strong_pass'; - 内网段授权写成
'192.168.50.%',而非'192.168.50.0/24'(MySQL不支持CIDR) - 本地调试需同时建两个账号:
'app_user'@'localhost'(socket)和'app_user'@'127.0.0.1'(TCP),二者权限不共享 - 应用连接字符串里写
host=127.0.0.1,就只认'app_user'@'127.0.0.1',别指望localhost账号能兜底
数据库名含_或%时,必须转义否则权限范围爆炸
GRANT语句中,db_1会被当作文本模式匹配,实际覆盖db01、dba1、db-1等所有单字符替换结果。这不是bug,是MySQL权限系统按LIKE规则解析的固有行为。
- 正确转义写法:
GRANT SELECT ON `db_1`.* TO 'app_user'@'192.168.50.42';(反斜杠+下划线) - 反斜杠在SQL字符串里要双写,所以实际执行时是
`db\_1`(客户端自动处理一层) - 验证是否成功:
SHOW GRANTS FOR 'app_user'@'192.168.50.42';输出中必须看到`db_1`,而不是`db_1` - 撤销权限时,
REVOKE语句必须与原始GRANT完全一致,包括反斜杠位置,否则静默失败
真正难的不是写对一条GRANT,而是所有带_或%的库名、表名,在每次授权/撤销时都得条件反射检查是否转义;以及每次加新用户,都要先查mysql.user有没有残留的@'%'——它不会报错,但会默默把你的安全策略变成摆设。


















