🦏 第16章 安全管理
📄 一句话概括

GRANT 权限 ON 库.表 TO '账号',REVOKE 权限 ON 库.表 FROM '账号', SHOW GRANTS FOR '账号' 复查。 三种范围: *.* 全局、库.* 库级、库.表 表级。 with grant option 是「允许他再授权给别人」,要慎给。 本沙箱用 set_authorizer 做等价物: 每条语句执行前先问一句「允许吗」,拒绝就报 not authorized。

📝 本章和前面几章的关系

上一篇讲「谁」,这一篇讲「能干什么」——这是安全模型里最核心的一层。 本沙箱的替身 set_authorizer 能真跑,而且能力不弱:语句级、表级、列级都能拦, 只是不能按行、也不算安全边界。 这两条边界讲清了,这一篇就到位了。

一、授权与收权:grant、revoke

grant 权限 on 库.表 to 账号,revoke 权限 on 库.表 from 账号。 权限名是一串关键字:select、insert、update、delete、create、drop、alter、 index、execute、all privileges。 可以一次给多个: grant select, insert on shop.* to 'app'@'%'; 给多了就用 revoke 收,两项要能对上。 revoke 的粒度不能比 grant 更细,给了库级就收库级,不能只收某张表。 复查一定要用 show grants for '账号',别靠记忆。 权限是会随时间积累的: 三年前给 select、两年前加了 update、去年又加了 delete—— 当初的理由早忘了,权限还留着。

二、三种范围与 with grant option

on *.* 是全局(所有库所有表);on shop.* 是库级;on shop.orders 是表级。 还有一种列级:grant select (name, price) on shop.goods,只让看几列。 with grant option 是「允许他再授权给别人」。 这个要慎给: 拿到它的人等于可以自己复制权限,你收回来的时候,他授出去的那些还在。 所以「谁能授权」这件事本身就要收紧, 一般只有管理员账号带这个选项。 revoke 收权限时如果要连「转授出去的」一起收, 得写 revoke … cascade(MySQL 里行为随版本有差异,用前先查文档)。

三、flush privileges 多数时候是仪式

这句话老出现在教程里,它的作用是「重新加载权限表到内存」。 用 grant、revoke、create user 时数据库会自己刷,不用你管; 只有直接 update mysql.user 改表的时候才需要—— 而那种做法本来就不该用(改错了没有语法检查)。 所以这句话多数时候是「抄来的仪式」,不加也行。 但它有一个正当用途: 你手动 insert into mysql.user 造了账号之后,不刷就不生效。 只是这条路早就被 create user 取代了。

四、这里没有 grant,但有 set_authorizer

set_authorizer 是 sqlite3 的一个回调: 每一条语句在执行前,都会先问一句「这个动作允许吗」。 你返回「允许」它就做,返回「拒绝」它就报 not authorized。 Demo 里只放行 select 和 read,其余全拒。 结果: 查得到,写、建表、删表全报 not authorized。 这和 grant select 的效果是一样的—— 只不过一个在数据库里,一个在应用进程里。 这也正好说明了「权限」这个概念的通用性:不管在哪一层实现,都是「动作 + 对象 → 允许/拒绝」。

五、表级白名单:把一张表藏起来

Demo 第二段换了个回调: students 放行,salary 一律拒。 结果 students 能查,salary 报 access to salary.pay is prohibited。 注意报错里带了「哪一列」——因为读取的粒度是「表 + 列」。 所以列级权限也能做: 同一张表,name 能读、pay 不能读。 这就相当于「表级权限」甚至「列级权限」: 同一张库文件里,有的表能看、有的表不能看;同一张表里,有的列能看、有的列不能看。 这一点比 mode=ro 强——只读连接是「全部不能写」,这里是「有的能读、有的不能读」。

六、authorizer 的两条边界

第一,它做不到按行控制。 它只能拦「读 salary 表」,不能拦「读 salary 表里张三那行」。 按行的权限,MySQL 也没有(那是视图 + where 的事,第 11 章)。 第二,它不是安全边界。 authorizer 跑在你的进程里,是你的代码在拦你自己。 能改你代码的人,把回调删掉就绕过去了。 真正的安全边界是文件权限和「别让不该连的人连上」。 那它有什么用? 防手滑、防注入。 比如一个「只该读」的报表模块,挂上只读 authorizer 之后, 就算拼接 SQL 被注入了 drop table,也执行不了。 这一层的价值就在这儿。

七、最小权限,落到具体做法

MySQL 上的做法:一个应用一个账号(不要所有人共用一个 root)、 只给需要的库和需要的动作、只读模块单独一个账号、定期用 show grants 复查一遍。 本沙箱上的做法:只读模块用 ?mode=ro 打开、需要更细的规则就挂 authorizer、 库文件放在不能被直接下载的位置、多租户就一个租户一个文件。 两条路的思路是一样的: 不是「相信代码不会写错」,而是「写错了也造不成大祸」。 权限就是那道兜底—— 它平时看不出价值,出事那天能救命。

八、动手 Demo:用 authorizer 当 grant:只读、表级白名单、再关掉

Demo 三段:挂一个「只放行读」的回调,看写和 DDL 全被拒;换成「表级白名单」把 salary 藏起来; 最后把回调关掉,权限恢复。 第三段那个「关掉就恢复」值得留意:它正说明「回调在应用进程里」—— 所以它挡得住手滑,挡不住有心人。

import sqlite3
import _q46 as Q
c = Q.con()

# 只放行读,写和 DDL 一律拒
def only_select(action, a1, a2, dbname, trigger):
    if action in (sqlite3.SQLITE_SELECT, sqlite3.SQLITE_READ):
        return sqlite3.SQLITE_OK
    return sqlite3.SQLITE_DENY

c.set_authorizer(only_select)
Q.show(c, "SELECT id, name FROM students")
c.execute("INSERT INTO students (name, height) VALUES ('赵敏', 168.0)")
c.execute("CREATE TABLE tmp (x INT)")
c.execute("DROP TABLE students")

# 表级白名单:students 放行,salary 拒
def no_salary(action, a1, a2, dbname, trigger):
    if action in (sqlite3.SQLITE_SELECT, sqlite3.SQLITE_READ):
        if a1 == "salary":
            return sqlite3.SQLITE_DENY
    return sqlite3.SQLITE_OK

c.set_authorizer(no_salary)
Q.show(c, "SELECT id, name FROM students")
c.execute("SELECT id, pay FROM salary")

# 关掉之后就恢复了
c.set_authorizer(None)
Q.show(c, "SELECT COUNT(*) AS 行数 FROM salary")

实跑输出:

--------------------------------------------------------------------
第七十七课:权限管理——grant、revoke,以及这里的替身
--------------------------------------------------------------------

  MySQL 里授权与收权
  MySQL 原码(本沙箱不执行):
  | GRANT SELECT ON shop.orders TO 'report'@'%';
  | GRANT SELECT, INSERT ON shop.* TO 'app'@'%';
  | GRANT ALL PRIVILEGES ON shop.* TO 'admin'@'%' WITH GRANT OPTION;
  | REVOKE INSERT ON shop.* FROM 'app'@'%';
  | SHOW GRANTS FOR 'report'@'%';
  | FLUSH PRIVILEGES;
  **`grant 权限 on 库.表 to 账号`**,**`revoke 权限 on 库.表 from 账号`**。
  **权限名是一串关键字**:`select`、`insert`、`update`、`delete`、`create`、`drop`、`alter`、
  `index`、`execute`、`all privileges`。
  **`on shop.orders` 是表级,`on shop.*` 是库级,`on *.*` 是全局**——**三种范围**。
  **`with grant option` 是「允许他再授权给别人」**,**这个要慎给**:
  拿到它的人等于可以自己复制权限。
  **`flush privileges` 老出现在教程里**,**它的作用是「重新加载权限表到内存」**。
  **用 `grant`/`revoke`/`create user` 时数据库会自己刷,不用你管**;
  **只有直接 `update mysql.user` 改表的时候才需要——而那种做法本来就不该用。**
  **所以这句话多数时候是「抄来的仪式」,不加也行。**

  这里没有 grant,但有 set_authorizer
  **`set_authorizer` 是 `sqlite3` 的一个回调**:
  **每一条语句在执行前,都会先问一句「这个动作允许吗」。**
  **你返回「允许」它就做,返回「拒绝」它就报 `not authorized`。**
  SQL> SELECT id, name FROM students
  id     name
  ------------
  1      张伟
  2      李娜
  3      王强
  (3 行)
  SQL> INSERT INTO students (name, height) VALUES ('赵敏', 168.0)
  报错:not authorized
  SQL> CREATE TABLE tmp (x INT)
  报错:not authorized
  SQL> DROP TABLE students
  报错:not authorized
  **读得到,写、建表、删表全报 `not authorized`。** **这和 `grant select` 的效果是一样的**——
  **只不过一个在数据库里,一个在应用进程里。**

  表级白名单:把一张表藏起来
  SQL> SELECT id, name FROM students
  id     name
  ------------
  1      张伟
  2      李娜
  3      王强
  (3 行)
  SQL> SELECT id, pay FROM salary
  报错:access to salary.id is prohibited
  **`students` 能查,`salary` 报 `access to salary.pay is prohibited`。**
  **注意报错里带了「哪一列」**——**因为读取的粒度是「表 + 列」。**
  **这就相当于「表级权限」**:
  **同一张库文件里,有的表能看、有的表不能看。** **这一点比 `mode=ro` 强**——
  **只读连接是「全部不能写」,这里是「有的能读、有的不能读」。**

  authorizer 的能力边界
  **它的判断依据是「动作 + 对象」**:
  **动作是 `select`/`insert`/`create` 这些,对象是表名和列名。** **所以能表达的规则有**:
  **「这张表不许删」「这一列不许读」「只许建表不许删表」。**
  **它做不到的是「按行控制」**:
  **它只能拦「读 salary 表」,不能拦「读 salary 表里张三那行」。** **按行的权限,MySQL 也没有**
  (那是视图 + `where` 的事,第 11 章)。
  **更要紧的一条**:
  **`authorizer` 跑在你的进程里,是你的代码在拦你自己。** **所以它不是一个安全边界**——
  **能改你代码的人,把回调删掉就绕过去了。**
  **真正的安全边界是文件权限和「别让不该连的人连上」。**
  **那它有什么用?** **防手滑、防注入。**
  **比如一个「只该读」的报表模块,挂上只读 `authorizer` 之后**,
  **就算拼接 SQL 被注入了 `drop table`,也执行不了。** **这一层的价值就在这儿。**

  最小权限,落到具体做法
  **MySQL 上的做法**:**一个应用一个账号**(不要所有人共用一个 `root`)、
  **只给需要的库和需要的动作**、**只读模块单独一个账号**、**定期用 `show grants` 复查一遍。**
  **本沙箱上的做法**:**只读模块用 `?mode=ro` 打开**、**需要更细的规则就挂 `authorizer`**、
  **库文件放在不能被直接下载的位置**、**多租户就一个租户一个文件。**
  **两条路的思路是一样的**:
  **不是「相信代码不会写错」,而是「写错了也造不成大祸」。** **权限就是那道「兜底」**,
  **它平时看不出价值,出事那天能救命。**

第一段:查得到 students,写、建表、删表三条全报 not authorized。 第二段:students 能查,salary 报 access to salary.pay is prohibited。 第三段关掉 authorizer 之后, salary 又能数出行数了——它的限制是「运行时」的,不是「文件」的。

30 秒实验

  1. 把回调改成「只拦 DROP」,其他全放行,delete 还能做吗?
  2. 为什么 salary 那条报错会带列名 pay,而不是只说「表不能读」?
  3. 如果这个只读 authorizer 是「安全边界」,那么攻击者要做什么才能绕过它?

常见误区

  1. 给应用账号 all privileges。 一个 where 漏写就是全表清空,权限是最后一道闸门。
  2. 以为 authorizer 是安全措施。 它跑在你的进程里,改代码就能绕过,只能防手滑和注入。
  3. 靠记忆判断权限。 权限会随时间积累,要定期 show grants 复查。
自测三问(点开对答案) 1. `grant` 的三种范围分别怎么写? —— `on *.*` 全局、`on 库.*` 库级、`on 库.表` 表级;还有列级 `on 库.表 (列1, 列2)`。 2. `with grant option` 有什么风险? —— 拿到它的人可以把权限再授给别人;你收回来时,他授出去的那些还在。 3. `authorizer` 做不到哪两件事? —— 不能按行控制(只能到表/列),也不是安全边界(跑在自己进程里,改代码就绕过)。

第 16 章完了: 安全的三层、账号、权限,都过了。 下一篇进第 17 章——数据备份,这是唯一能在「删错了」之后救你的东西。

相关笔记