最小权限法

引言

为了最大限度地降低 SQL 注入攻击成功后可能造成的损失,您应该尽可能减少环境中每个数据库帐户的权限。首先要确定应用程序帐户需要哪些访问权限,而不是事后才去想需要撤销哪些访问权限。

所以我们只分配能完成相应任务的最小权限。切勿为应用程序帐户分配 DBA 或管理员类型的访问权限。

任何攻击方式(不仅是 SQL 注入)都存在一个底层逻辑:攻击者利用的是系统本身赋予连接的权限——那个持有权限的账户不是攻击者,但攻击者的语句通过它被执行。因此,限制连接的权限,就是把攻击者的"天花板"压到最低。

来源:http://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html#least-privilege

两层权限体系

最小权限法要管的不是"一层",而是两层相互独立的权限系统同时收紧

情况 A:SQL root + OS SYSTEM           = 数据库删光 + 文件系统随便逛   最差
情况 B:SQL 写权限 + OS NETWORK SERVICE = 能改业务表,但碰不到系统文件 
情况 C:SQL 只读 + OS SYSTEM           = 数据库安全,但 LOAD_FILE 能偷系统文件 
情况 D:SQL 只读 + OS NETWORK SERVICE  = 炸了也只能看自己的表  最安全

第一层:MySQL 内部账户权限

系统账户 & MySQL 账户的区别

操作系统账户 MySQL 内部账户
叫什么 NETWORK SERVICE / SYSTEM root@localhost
在哪定义的 Windows 用户管理器 / 服务属性 MySQL 的 mysql.user
怎么登录 服务控制面板 mysql -uroot -p
管什么 读写文件、绑定端口、启动进程 创建数据库、查表、授权、改密码
SQL 注入能不能利用 能——LOAD_FILE() 的结果取决于 OS 账户 能——攻击者通过应用连接的身份操作数据库

为什么要分开说

任何程序执行前,Windows 都先决定"以谁的身份运行",然后给进程挂一个令牌(token)。mysqld.exe 之后所有操作(LOAD_FILE、写日志、读写文件),都受这个令牌的限制。

然而以什么身份运行不是 MySQL 自己能决定的——是安装程序在注册 Windows 服务时写的配置。安装器默认填 SYSTEM(旧版 MySQL 5.x 很常见)。

SYSTEM 身份运行意味着什么

mysqld.exe(进程本体)
    ↓
    运行之前,Windows 给它挂一个"令牌"
    ↓
    令牌上写着:● 你是 SYSTEM
              ● 你能读:C:\Windows\System32\ ✓
              ● 你能写:C:\inetpub\wwwroot\ ✓
              ● 你能关停其他进程 ✓
              ● 你能加载驱动 ✓
    ↓
    mysqld.exe 的所有操作(LOAD_FILE、写日志……)
    都受这个令牌的限制:SYSTEM → 全机器为所欲为

第二层:更改 MySQL 的 OS 运行账户

现在的 MySQL 默认

MySQL 8.0 安装器默认使用 网络服务(NETWORK SERVICE),而非本地系统(SYSTEM)。这是 MySQL 自己做的安全加固。

如何验证 / 更改(GUI 操作)

  1. Win + R → 输入 services.msc → 回车
  2. 找到 MySQL80 → 右键 → 属性登录 选项卡
  3. 当前显示的账户就是 mysqld 的 OS 运行身份

如果显示"本地系统账户"(即 SYSTEM),改成 NETWORK SERVICE

  1. 先给新账户加 Data 目录权限:右键 C:\ProgramData\MySQL\MySQL Server 8.0\Data → 属性 → 安全 → 编辑 → 添加 → 输入 NETWORK SERVICE → 检查名称 → 勾选"完全控制" → 确定
  2. 回到 services.msc → MySQL80 属性 → 登录 → 选此账户 → 输入 NT AUTHORITY\NETWORK SERVICE → 密码框留空(内置账户不需要密码) → 确定
  3. 右键 MySQL80 → 重新启动

两种账户的能力对比

能力 SYSTEM NETWORK SERVICE
读写 MySQL Data 目录
绑定 3306 端口
C:\Windows\System32\SAM
写 Web 目录(如 C:\inetpub\
改注册表系统项
关闭其他进程

Windows 自身也在保护 SAM

SAM 文件不仅被 MySQL 的 SYSTEM/NETWORK SERVICE 权限管控——Windows 自己还额外加了三层保护:

第 1 层:文件管理器隐藏 SAM(连看都看不到)
第 2 层:NTFS ACL 只允许 SYSTEM 和 TrustedInstaller 读取
第 3 层:文件被操作系统锁定,无法删除/替换
第 4 层:密码不是明文——存的哈希 + 盐

你把 MySQL 改成 NETWORK SERVICE 后,SELECT LOAD_FILE('C:/Windows/System32/config/SAM') 返回 NULL——这条 2005 年的经典注入攻击路径在 2026 年已经死透了。

有一句话概括得很好——不需要改一行 SQL 配置,改一个 Windows 服务属性就能把攻击面砍掉大半

MySQL 权限基础

权限的三个级别

级别 范围 示例
全局 所有库所有表 GRANT SELECT ON *.* TO ...
数据库 指定库下所有表 GRANT SELECT ON runoob.* TO ...
对象级 指定库的某张表 GRANT SELECT ON runoob.users TO ...

ON 后面的语法

写法 意思
*.* 所有数据库的所有表
mydb.* mydb 库下所有表
mydb.users mydb 库的 users 这一张表
mydb.user% mydb 库内以 user 开头的所有表

原则:能 mydb.* 就别 *.*,能 mydb.users 就别 mydb.*。范围越小,注入后损失越小。

静态权限 vs 动态权限

静态权限 动态权限(启动时定义) 动态权限(组件定义)
定义者 服务器源码写死 mysqld 启动时注册 组件/插件安装时注册
存储位置 mysql.user 固定列 user_attributes JSON 列 user_attributes JSON 列
是否总可用 总是 总是 组件卸载即消失
例子 SELECT/INSERT/SUPER BACKUP_ADMIN / SET_USER_ID FIREWALL_ADMIN

误区:“动态权限 = 插件卸载就消失”。实际上大多数动态权限是启动时定义的,永远可用。真正随组件消失的只有企业版那几个(防火墙、审计、脱敏)。


常用权限管理命令

-- 查看所有用户
SELECT user, host FROM mysql.user;

-- 查看某个用户的完整授权
SHOW GRANTS FOR 'test'@'localhost';

-- 查看当前登录用户
SELECT USER();

-- 创建用户(自动带 USAGE)
CREATE USER 'test'@'localhost' IDENTIFIED BY 'strong_password';

-- 授予库级别只读
GRANT SELECT ON runoob.* TO 'test'@'localhost';

-- 撤销权限
REVOKE SELECT ON runoob.* FROM 'test'@'localhost';

-- 使权限变更生效(GRANT/REVOKE 后通常会自动,但手动刷新无害)
FLUSH PRIVILEGES;

USAGE 的含义

USAGE = 没有任何权限,但允许连接CREATE USER 自动授予:

能做什么         mysql -utest -p 连上 3306
	            SELECT USER() -- 看自己是谁
                查表、建库、SHOW DATABASES

实践

参考 MySQL 官方文档:添加账号、分配权限、删除账号

创建一个受限账户

CREATE USER 'test'@'localhost' IDENTIFIED BY '1111111';
GRANT USAGE, SELECT ON runoob.* TO 'test'@'localhost';
SHOW GRANTS FOR 'test'@'localhost';

输出:

GRANT USAGE ON *.* TO `test`@`localhost`
GRANT SELECT ON `runoob`.* TO `test`@`localhost`

test 账户连接测试:

SELECT * FROM users;  --  能查

INSERT INTO users VALUES (null, 'Aim', 21, '2005-6-30', 1);
-- ❌ ERROR 1142: INSERT command denied

表级权限 + USE 的问题

如果你只给一张表的权限:

GRANT SELECT ON runoob.users TO 'test'@'localhost';

此时 USE runoob 会失败——USE 是库级别操作,需要该库上至少有一条数据库级权限记录。两种解法:

-- 方案 A:直接给库级 SELECT(推荐)
GRANT SELECT ON runoob.* TO 'test'@'localhost';

-- 方案 B:保持表级,加一个无害的库级权限"开门"
GRANT CREATE TEMPORARY TABLES ON runoob.* TO 'test'@'localhost';

PHP 连接时如果不在 new mysqli() 里指定库名(第四个参数留空),用 SELECT * FROM runoob.users(带库名前缀)也能绕过这个问题。

验证系统权限隔离

-- NETWORK SERVICE 应该读不到系统文件
SELECT LOAD_FILE('C:/Windows/System32/config/SAM');
-- 预期:NULL

-- 对比:MySQL 自己的日志能读到(NETWORK SERVICE 有 Data 目录权限)
SELECT LOAD_FILE('C:/ProgramData/MySQL/MySQL Server 8.0/Data/DESKTOP-F1IJQQ4.err');
-- 预期:返回内容(可能被 secure_file_priv 限制)

攻击路径的历史变迁

2005 年:LOAD_FILE('C:\boot.ini') → 经典注入教学案例
2010 年:secure_file_priv 默认非空 → 路径开始收窄
2018 年:MySQL 8.0 默认不给 FILE 权限 → 默认安全
2020 年:云数据库 / 容器化 → 根本没有 SAM 可读
2026+:NETWORK SERVICE 默认 → 连系统目录都进不去

这条攻击路径从"入门教程必讲"变成了"只有 CTF 比赛才出现的极端场景"——不是因为注入少了,是因为防御层把最值钱的系统提权路径堵到没人走了。


核心思想:纵深防御

安全设计的原则不是写出没有漏洞的代码——那是不可能的。而是:

系统安全性 ≠ 漏洞数量为 0
系统安全性 = 攻击成功所需的最短链长 × 攻破后的最大收益

链越长、收益越小 → 系统越安全。两者的乘积趋近于零,就是一个工程上等价于"完美"的系统。

每一层防御单独拿出来都不完美。四层垒在一起(参数化 + 白名单 + 存储过程 + 最小权限),攻击链长到攻击者放弃——他会去试其他攻击面(XSS、越权、CSRF),而不是死磕 SQL 注入。


注意事项(实践中的坑)

  1. USE runoob 权限拒绝:给用户 GRANT SELECT ON runoob.users(表级别)之后,USE runoob 需要库级别权限。可以用 SELECT * FROM runoob.users(带库名前缀)绕过,或者加一个无害的库级权限(如 CREATE TEMPORARY TABLES)来"开门"。

  2. 文件管理器找不到 SAM / SYSTEM 文件:Windows 默认隐藏受保护的系统文件。需要勾选"查看 → 隐藏的项目"并取消勾选"隐藏受保护的操作系统文件"。但即使显示出来,普通账户 luck 也打不开——NTFS ACL 拒绝访问。

  3. Data 目录权限拒绝C:\ProgramData\MySQL\MySQL Server 8.0\Data 默认只允许 SYSTEM / NETWORK SERVICE 访问,普通用户 shell 没有读取权限。需要用管理员权限的终端才能直接查看日志文件。

  4. "只有 DBA 能连接"不能代替 OS 权限:SQL 注入走的是 Web 应用的合法连接通道——攻击者不需要 mysql -uroot -p,他的恶意 SQL 通过应用账户发进来。DBA 管控的是"入口",堵不住"已经进门的合法连接被注入"。

  5. 改 MySQL 服务账户时密码框不要填NETWORK SERVICE 是 Windows 系统内置账户,不由用户管理密码——密码框留空即可,Windows 会自动处理认证。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐