作者:jiyf
复现问题
- 创建了一个用户 abc
- use information_schema
- 给新建的 abc 添加 insert 权限失败,提示权限验证失败
mysql> select @@version;
+---------------------+
| @@version |
+---------------------+
| 5.7.25-TiDB-v4.0.11 |
+---------------------+
1 row in set (0.00 sec)
mysql> create user 'abc'@'%';
Query OK, 0 rows affected (0.01 sec)
mysql> use information_schema
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> grant insert on *.* to 'abc'@'%';
ERROR 8121 (HY000): privilege check fail
正常情况下这里不应该报错的,以下的操作就没有问题,这也是期望的结果:
Type 'help;' or '\"h' for help. Type '\"c' to clear the current input statement.
mysql> grant insert on *.* to 'abc'@'%';
Query OK, 0 rows affected (0.01 sec)
mysql> use test
Database changed
mysql> grant insert on *.* to 'abc'@'%';
Query OK, 0 rows affected (0.01 sec)
只要在 information_schema,就会报错:
mysql> use information_schema
Database changed
mysql> grant insert on *.* to 'abc'@'%';
ERROR 8121 (HY000): privilege check fail
排查问题
怀疑是下面的原因导致的问题:
- use information_schema 使得当前会话的当前 DB 为 information_schema
- information_schema 非用户表,所以不允许 update 权限
- 在权限检查时候,指定的赋权范围是 *.*,但是权限检查却错误的检查了当前 DB information_schema
顺着这个思路查看 TiDB 源码,翻到下面一段:
func (p *UserPrivileges) RequestVerification(activeRoles []*auth.RoleIdentity, db, table, column string, priv mysql.PrivilegeType) bool {
...
// Skip check for system databases.
// See https://dev.mysql.com/doc/refman/5.7/en/information-schema.html
dbLowerName := strings.ToLower(db)
switch dbLowerName {
// 如果 db 是 InformationSchema,且权限检查是 mysql.UpdatePri,那这里就返回验证权限失败
case util.InformationSchemaName.L:
switch priv {
case mysql.CreatePriv, mysql.AlterPriv, mysql.DropPriv, mysql.IndexPriv, mysql.CreateViewPriv,
mysql.InsertPriv, mysql.UpdatePriv, mysql.DeletePriv:
return false
}
return true
// We should be very careful of limiting privileges, so ignore `mysql` for now.
case util.PerformanceSchemaName.L, util.MetricSchemaName.L:
if (dbLowerName == util.PerformanceSchemaName.L && perfschema.IsPredefinedTable(table)) ||
(dbLowerName == util.MetricSchemaName.L && infoschema.IsMetricTable(table)) {
switch priv {
case mysql.CreatePriv, mysql.AlterPriv, mysql.DropPriv, mysql.IndexPriv, mysql.InsertPriv, mysql.UpdatePriv, mysql.DeletePriv:
return false
case mysql.SelectPriv:
return true
}
}
}
...
}
这里对系统表 InformationSchema、PerformanceSchema 进行了权限限制。从代码上看,如果验证用户对 information_schema 执行 update 权限,那返回不通过。
mysql> grant insert on information_schema.* to 'abc'@'%';
ERROR 8121 (HY000): privilege check fail
这里尝试给用户在 information_schema 库上添加 insert 权限,返回权限不通过,符合预期。
所以接下来排查,在 use information_schema 情况下,哪里错把 information_schema 作为了 *.* 的验证参数。
打开 planbuilder.go,看下 grant 操作需要对哪些权限进行验证:
func (b *PlanBuilder) buildSimple(node ast.StmtNode) (Plan, error) {
p := &Simple{Statement: node}
switch raw := node.(type) {
...
case *ast.GrantStmt:
if b.ctx.GetSessionVars().CurrentDB == "" && raw.Level.DBName == "" {
if raw.Level.Level == ast.GrantLevelTable {
return nil, ErrNoDB
}
}
b.visitInfo = collectVisitInfoFromGrantStmt(b.ctx, b.visitInfo, raw)
...
}
func collectVisitInfoFromGrantStmt(sctx sessionctx.Context, vi []visitInfo, stmt *ast.GrantStmt) []visitInfo {
// To use GRANT, you must have the GRANT OPTION privilege,
// and you must have the privileges that you are granting.
dbName := stmt.Level.DBName
tableName := stmt.Level.TableName
// 这里就是出现问题的原因
if dbName == "" {
dbName = sctx.GetSessionVars().CurrentDB
}
vi = appendVisitInfo(vi, mysql.GrantPriv, dbName, tableName, "", nil)
...
}
从代码上看,如果 stmt.Level.DBName 是空,那么就赋值 CurrentDB。
所以如果对 *.* 处理后,认为 stmt.Level.DBName 是空,那么就赋值 CurrentDB(information_schema ),后面自然就出现验证不通过的情景。
在这里添加一条日志:
case *ast.GrantStmt:
if b.ctx.GetSessionVars().CurrentDB == "" && raw.Level.DBName == "" {
if raw.Level.Level == ast.GrantLevelTable {
return nil, ErrNoDB
}
}
logutil.BgLogger().Error("hello", zap.String("CurrentDB", b.ctx.GetSessionVars().CurrentDB), zap.String("rawDB", raw.Level.DBName), zap.String("rawTable", raw.Level.TableName))
b.visitInfo = collectVisitInfoFromGrantStmt(b.ctx, b.visitInfo, raw)
启动后执行以下 mysql 操作:
mysql> use information_schema
Database changed
mysql> grant insert on *.* to 'abc'@'%';
ERROR 8121 (HY000): privilege check fail
查看日志看到下面的记录:
[2021/06/24 14:09:27.698 +08:00] [ERROR] [planbuilder.go:2019] [hello] [CurrentDB=information_schema] [rawDB=] [rawTable=]
这里果然对于 *.* 把 raw.Level.DBName、raw.Level.TableName,都作为空置对待,然后出现 dbName = sctx.GetSessionVars().CurrentDB,导致验证失败。
再添加一条日志:
dbLowerName := strings.ToLower(db)
logutil.BgLogger().Error("hello world", zap.String("db", dbLowerName), zap.String("table", table), zap.String("user", p.user), zap.String("host", p.host), zap.Uint64("priv", uint64(priv)))
switch dbLowerName {
case util.InformationSchemaName.L:
switch priv {
case mysql.CreatePriv, mysql.AlterPriv, mysql.DropPriv, mysql.IndexPriv, mysql.CreateViewPriv,
mysql.InsertPriv, mysql.UpdatePriv, mysql.DeletePriv:
return false
}
看到以下日志:
[2021/06/24 14:05:03.419 +08:00] [ERROR] [privileges.go:60] [hello world] [db=information_schema] [table=] [user=root] [host=%] [priv=4096]
[2021/06/24 14:05:03.419 +08:00] [ERROR] [privileges.go:60] [hello world] [db=information_schema] [table=] [user=root] [host=%] [priv=8]
其中 priv=4096 是 GrantPriv,priv=8 就是 InsertPriv,到这里就验证不通过,添加权限失败。
安全权限问题
给用户赋权需要以下条件:
- 进行赋权的用户带有 grant 权限
- 只能对自己拥有的权限赋给其他用户,比如自己只有 select 权限,那自然没有权利去给其他用户添加 insert 权限。
进行 grant 操作时候,权限检查实际就是检查赋权人是否满足以上条件。
结合以上几个方面,可以实现一个带有 grant 权限的非超级用户达到扩展自己权限的目的。
扩大权限范围
扩大权限方法如下:
- grant priv on *.* to ‘user’@‘host’,参考日志,*.* 会使得 [rawDB=] [rawTable=]
- 如果当前 CurrentDB 不为空,那么验证权限的 db 就被设置为 CurrentDB
- 然后会对当前用户对 CurrentDB 进行 priv 权限验证
- 而当前用户拥有对 CurrentDB 的 priv 权限
- 那么权限验证成功
- 给 ‘user’@‘host’ 添加权限成功,而这些 priv 权限是对所有库表,超过 CurrentDB。
创建用户 ‘abc’@’%’,给 Select,Insert,Update,Delete,GRANT 权限:
mysql> create user 'abc'@'%' identified by '123';
Query OK, 0 rows affected (0.02 sec)
mysql> grant insert,update,delete,select on test.* to 'abc'@'%' with grant option;
Query OK, 0 rows affected (0.01 sec)
mysql> show grants for 'abc'@'%';
+----------------------------------------------------------------------------+
| Grants for abc@% |
+----------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'abc'@'%' |
| GRANT Select,Insert,Update,Delete ON test.* TO 'abc'@'%' WITH GRANT OPTION |
+----------------------------------------------------------------------------+
2 rows in set (0.00 sec)
使用 ‘abc’@’%’ 登录数据库,可以看到它拥有的权限:
mysql> show grants;
+----------------------------------------------------------------------------+
| Grants for User |
+----------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'abc'@'%' |
| GRANT Select,Insert,Update,Delete ON test.* TO 'abc'@'%' WITH GRANT OPTION |
+----------------------------------------------------------------------------+
2 rows in set (0.00 sec)
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| INFORMATION_SCHEMA |
| test |
+--------------------+
2 rows in set (0.00 sec)
对它的权限进行扩充是被禁止的,这符合预期:
mysql> GRANT Select,Insert,Update,Delete ON *.* TO 'abc'@'%';
ERROR 8121 (HY000): privilege check fail
mysql> show grants;
+----------------------------------------------------------------------------+
| Grants for User |
+----------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'abc'@'%' |
| GRANT Select,Insert,Update,Delete ON test.* TO 'abc'@'%' WITH GRANT OPTION |
+----------------------------------------------------------------------------+
2 rows in set (0.00 sec)
执行以下方法就可以顺利扩充它的权限:
mysql> use test
Database changed
mysql> GRANT Select,Insert,Update,Delete ON *.* TO 'abc'@'%';
Query OK, 0 rows affected (0.02 sec)
mysql> show grants;
+----------------------------------------------------------------------------+
| Grants for User |
+----------------------------------------------------------------------------+
| GRANT Select,Insert,Update,Delete ON *.* TO 'abc'@'%' |
| GRANT Select,Insert,Update,Delete ON test.* TO 'abc'@'%' WITH GRANT OPTION |
+----------------------------------------------------------------------------+
2 rows in set (0.00 sec)
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| INFORMATION_SCHEMA |
| METRICS_SCHEMA |
| PERFORMANCE_SCHEMA |
| mysql |
| sbtest
| test |
| tpcc |
+--------------------+
10 rows in set (0.00 sec)
可以看到 ’abc’@’%’ 拥有了全部库表 Select,Insert,Update,Delete 权限,实现权限扩充。
新增权限种类
再回头看下这段代码:
dbLowerName := strings.ToLower(db)
switch dbLowerName {
case util.InformationSchemaName.L:
switch priv {
case mysql.CreatePriv, mysql.AlterPriv, mysql.DropPriv, mysql.IndexPriv, mysql.CreateViewPriv,
mysql.InsertPriv, mysql.UpdatePriv, mysql.DeletePriv:
return false
}
return true
当检查 InformationSchema 库的 mysql.CreatePriv, mysql.AlterPriv, mysql.DropPriv, mysql.IndexPriv, mysql.CreateViewPriv,mysql.InsertPriv, mysql.UpdatePriv, mysql.DeletePriv
这些权限时候,返回失败;而其他权限直接返回成功,比如 Super、Select、Create User 等等。
通过以下方法实现用户授权自己没有的 Super、Select、Create User 等权限:
- grant Super、Select、Create User on *.* to ‘user’@‘host’,参考日志,*.* 会使得 [rawDB=] [rawTable=]
- 如果当前 CurrentDB 不为空,那么验证权限的 db 就被设置为 CurrentDB
- 然后会对当前用户对 CurrentDB 进行 priv 权限验证
- 而 CurrentDB 为 InformationSchema,priv 为 Super、Select、Create User,上面验证直接返回通过
- 那么权限验证成功
- 给 ‘user’@‘host’ 添加 Super、Select、Create User 权限成功,而这些 priv 权限赋权用户本来是没有的,它实现了对未拥有权限进行授权。
创建用户 ‘abc’@’%’,给 Select,GRANT 权限:
mysql> create user 'abc'@'%' identified by '123';
Query OK, 0 rows affected (0.01 sec)
mysql> grant select on test.* to 'abc'@'%' with grant option;
Query OK, 0 rows affected (0.01 sec)
mysql> show grants for 'abc'@'%';
+-------------------------------------------------------+
| Grants for abc@% |
+-------------------------------------------------------+
| GRANT USAGE ON *.* TO 'abc'@'%' |
| GRANT Select ON test.* TO 'abc'@'%' WITH GRANT OPTION |
+-------------------------------------------------------+
2 rows in set (0.00 sec)
使用 ‘abc’@’%’ 登录数据库,可以看到它拥有的权限:
mysql> show grants;
+-------------------------------------------------------+
| Grants for User |
+-------------------------------------------------------+
| GRANT USAGE ON *.* TO 'abc'@'%' |
| GRANT Select ON test.* TO 'abc'@'%' WITH GRANT OPTION |
+-------------------------------------------------------+
2 rows in set (0.00 sec)
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| INFORMATION_SCHEMA |
| test |
+--------------------+
2 rows in set (0.00 sec)
尝试创建新用户,但是被禁止,因为 Create User 权限,所以失败符合预期;给自己添加 Create User 权限失败也符合预期:
mysql> create user 'efg'@'%';
ERROR 1227 (42000): Access denied; you need (at least one of) the CREATE User privilege(s) for this operation
mysql> grant CREATE User on test.* to 'abc'@'%';
ERROR 8121 (HY000): privilege check fail
执行以下方法就可以顺利添加 Create User,并能创建新用户:
mysql> use information_schema
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> grant CREATE User on *.* to 'abc'@'%';
Query OK, 0 rows affected (0.01 sec)
mysql> show grants;
+-------------------------------------------------------+
| Grants for User |
+-------------------------------------------------------+
| GRANT Create User ON *.* TO 'abc'@'%' |
| GRANT Select ON test.* TO 'abc'@'%' WITH GRANT OPTION |
+-------------------------------------------------------+
2 rows in set (0.00 sec)
mysql> create user 'efg'@'%';
Query OK, 0 rows affected (0.01 sec)
问题原因
出现问题的根本原因应该还是在这一步:
func collectVisitInfoFromGrantStmt(sctx sessionctx.Context, vi []visitInfo, stmt *ast.GrantStmt) []visitInfo {
// To use GRANT, you must have the GRANT OPTION privilege,
// and you must have the privileges that you are granting.
dbName := stmt.Level.DBName
tableName := stmt.Level.TableName
// 这里就是出现问题的原因
if dbName == "" {
dbName = sctx.GetSessionVars().CurrentDB
}
vi = appendVisitInfo(vi, mysql.GrantPriv, dbName, tableName, "", nil)
...
}
TiDB 授权分为三种类型:
类型
| 说明
|
GrantLevelGlobal
| *.*
|
grantDBLevel
| test.*
|
GrantLevelTable
| test.t1
|
对于 GrantLevelGlobal 形式,dbName 为空,就会出现前面的问题。所以这里就要加判断,对于 GrantLevelGlobal 情形保持 dbName 为空即可。
但是这还不能解决所有问题,以 MySQL 5.7 做对比,执行以下语句:
Server version: 5.7.36-log MySQL Community Server (GPL)
Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\"h' for help. Type '\"c' to clear the current input statement.
mysql> grant select on * to 'bcd'@'%';
ERROR 1046 (3D000): No database select
在 tidb 上,用户却赋权成功,不过 GRANT Select ON .* TO ‘abc’@’%’ 这是一个无效的权限。
Type 'help;' or '\"h' for help. Type '\"c' to clear the current input statement.
mysql> grant select on * to 'abc'@'%';
Query OK, 0 rows affected (0.01 sec)
mysql> show grants for 'abc'@'%';
+---------------------------------+
| Grants for abc@% |
+---------------------------------+
| GRANT USAGE ON *.* TO 'abc'@'%' |
| GRANT Select ON .* TO 'abc'@'%' |
+---------------------------------+
2 rows in set (0.00 sec)
mysql> select @@version;
+---------------------+
| @@version |
+---------------------+
| 5.7.25-TiDB-v4.0.11 |
+---------------------+
1 row in set (0.00 sec)
TIDB 行为在于对 grant select on * to ‘abc’@’%’ 语句,解析为类型 grantDBLevel,而 [rawDB=] [rawTable=],* 被改写为 CurrentDB.* ,所以 CurrentDB 为空时候出现上面 GRANT Select ON .* TO ‘abc’@’%’ 权限情况:
case *ast.GrantStmt:
if b.ctx.GetSessionVars().CurrentDB == "" && raw.Level.DBName == "" {
// if raw.Level.Level == ast.GrantLevelTable {
if raw.Level.Level == ast.GrantLevelTable || raw.Level.Level == ast.GrantLevelDB {
return nil, ErrNoDB
}
}
b.visitInfo = collectVisitInfoFromGrantStmt(b.ctx, b.visitInfo, raw)
在这里添加检查 GrantLevelDB 情况,直接返回报错,跟 MySQL 行为保持一致。
总结
- 修改buildSimple 函数,在 GrantLevelDB 情况下,db 为空时候保持与 MySQL 一致直接报错。
func (b *PlanBuilder) buildSimple(node ast.StmtNode) (Plan, error) { p := &Simple{Statement: node} switch raw := node.(type) { … case *ast.GrantStmt: if b.ctx.GetSessionVars().CurrentDB == “” && raw.Level.DBName == “” { // if raw.Level.Level == ast.GrantLevelTable { if raw.Level.Level == ast.GrantLevelTable || raw.Level.Level == ast.GrantLevelDB { return nil, ErrNoDB } } b.visitInfo = collectVisitInfoFromGrantStmt(b.ctx, b.visitInfo, raw) …} - 修改 collectVisitInfoFromGrantStmt 函数,在 GrantLevelGlobal 情况下,不设置 dbName = sctx.GetSessionVars().CurrentDB。
func collectVisitInfoFromGrantStmt(sctx sessionctx.Context, vi []visitInfo, stmt *ast.GrantStmt) []visitInfo { // To use GRANT, you must have the GRANT OPTION privilege, // and you must have the privileges that you are granting. dbName := stmt.Level.DBName tableName := stmt.Level.TableName if dbName == "" && (stmt.Level.Level == ast.GrantLevelTable || stmt.Level.Level == ast.GrantLevelDB){ dbName = sctx.GetSessionVars().CurrentDB } vi = appendVisitInfo(vi, mysql.GrantPriv, dbName, tableName, "", nil) ...}
其他
revoke 可能存在类型问题。