作者:jiyf​

复现问题

  1. 创建了一个用户 abc
  2. use information_schema
  3. 给新建的 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



排查问题

怀疑是下面的原因导致的问题:

  1. use information_schema 使得当前会话的当前 DB 为 information_schema
  2. information_schema 非用户表,所以不允许 update 权限
  3. 在权限检查时候,指定的赋权范围是 *.*,但是权限检查却错误的检查了当前 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,到这里就验证不通过,添加权限失败。



安全权限问题

给用户赋权需要以下条件:

  1. 进行赋权的用户带有 grant 权限
  2. 只能对自己拥有的权限赋给其他用户,比如自己只有 select 权限,那自然没有权利去给其他用户添加 insert 权限。

进行 grant 操作时候,权限检查实际就是检查赋权人是否满足以上条件。

结合以上几个方面,可以实现一个带有 grant 权限的非超级用户达到扩展自己权限的目的。



扩大权限范围

扩大权限方法如下:

  1. grant priv on *.* to ‘user’@‘host’,参考日志,*.* 会使得 [rawDB=] [rawTable=]
  2. 如果当前 CurrentDB 不为空,那么验证权限的 db 就被设置为 CurrentDB
  3. 然后会对当前用户对 CurrentDB 进行 priv 权限验证
  4. 而当前用户拥有对 CurrentDB 的 priv 权限
  5. 那么权限验证成功
  6. 给 ‘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 等权限:

  1. grant Super、Select、Create User on *.* to ‘user’@‘host’,参考日志,*.* 会使得 [rawDB=] [rawTable=]
  2. 如果当前 CurrentDB 不为空,那么验证权限的 db 就被设置为 CurrentDB
  3. 然后会对当前用户对 CurrentDB 进行 priv 权限验证
  4. 而 CurrentDB 为 InformationSchema,priv 为 Super、Select、Create User,上面验证直接返回通过
  5. 那么权限验证成功
  6. 给 ‘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 行为保持一致。



总结

  1. 修改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) …}
  2. 修改 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 可能存在类型问题。