仅执行GRANT不能完成库级隔离,因未收回SHOW DATABASES权限会导致租户可见所有库名,且应用层SQL拼接库名易引发跨库注入;必须显式REVOKE SHOW DATABASES并强制连接串指定database参数。

为什么不能只靠GRANT就认为库级隔离完成了
很多人执行完 GRANT SELECT, INSERT ON `tenant_a`.* TO 'app_a'@'%' 就以为租户A只能碰自己的库,但现实是:只要没显式收回 SHOW DATABASES 权限,app_a 仍能执行 SHOW DATABASES 看到所有租户库名;更危险的是,如果应用层用字符串拼接库名(比如 "SELECT * FROM " + tenantDb + ".orders"),攻击者可能通过租户ID注入绕过权限检查,直接查 tenant_b.orders。
必须显式禁用 SHOW DATABASES 并绑定连接库名
MySQL 默认允许普通用户执行 SHOW DATABASES,只要账号有任意库权限。这不是漏洞,是设计行为——但对多租户就是风险点。
- 创建租户账号后,立刻执行:
REVOKE SHOW DATABASES ON *.* FROM 'tenant_a_app'@'%' - 应用连接串中必须带
database=tenant_a参数(JDBC 里是useSSL=false&database=tenant_a),不能依赖USE tenant_a切库 - 连接池初始化时就指定库名,避免后续任何
USE语句切换上下文
租户库命名与权限授权的实操细节
库名不带业务含义、不暴露租户真实标识,是基础安全习惯。权限粒度也要卡死,不能图省事用 ALL PRIVILEGES ON *.* 或 GRANT ... ON `tenant_%`.* 这类模糊匹配。
- 库名建议格式:
tenant_abc_prod(含环境后缀)、t12345_staging(数字ID+环境),避免用公司名、域名等可推断信息 - 授权必须精确到单库:
GRANT SELECT, INSERT, UPDATE, DELETE ON `tenant_abc_prod`.* TO 'tenant_abc_app'@'%' - 绝对禁止授予
DROP、ALTER、CREATE VIEW、PROCESS等高危权限 - MySQL 8.0+ 可建角色统一管理:
CREATE ROLE 'tenant_rw'; GRANT SELECT, INSERT, UPDATE, DELETE ON `tenant_abc_prod`.* TO 'tenant_rw';,再把角色赋给用户
最容易被忽略的两个破坏点:应用层SQL拼接和备份脚本
权限配置再严,只要应用代码里出现硬编码库名或动态拼接,整个隔离就形同虚设。备份脚本若依赖 SHOW DATABASES 自动发现库,也会在无意中暴露租户结构。
- 禁止在 Java/Python 中写类似
"SELECT * FROM " + tenantDb + ".orders"的代码,一律改用预编译参数或 ORM 的 schema 绑定机制 - 备份脚本必须从白名单配置读取租户库名列表,而不是调用
SHOW DATABASES后过滤 - ORM 如 MyBatis 不要依赖
@Select("SELECT * FROM ${tenant}.orders")这种字符串替换,改用DatabaseIdProvider或多数据源路由
真正的库级隔离不是配完 GRANT 就结束,而是从账号创建、连接初始化、SQL 构造、运维脚本全链路堵住跨库可能性。最常出问题的地方,永远不在数据库配置里,而在应用代码那行看似无害的字符串拼接上。


















