很多 PHP 网站能够正常运行,并不代表数据库权限设计就是安全的。最常见的问题之一,就是程序配置文件直接使用 MySQL root 账号连接数据库。开发阶段这样做非常方便,建表、修改结构、读写数据全部不用考虑权限问题,但一旦网站代码、配置文件或者服务器本身出现漏洞,攻击者拿到的就不是一个普通应用账号,而可能是一把能够控制整个 MySQL 实例的钥匙。
MySQL 的权限系统本身就是为了避免这种情况。一个账户到底能做什么,由用户名、连接来源以及被授予的权限共同决定。MySQL 官方提供 CREATE USER、GRANT、REVOKE、DROP USER 等语句管理账户,并可以通过 SHOW GRANTS 查看实际权限。
因此,网站程序最基本的原则应该是:root 用来管理数据库,应用程序使用专门的应用账号。
假设网站使用数据库 mnewstv,PHP 应用运行在数据库服务器本机,可以创建一个专门的账号:
CREATE USER 'mnewstv_app'@'localhost'
IDENTIFIED BY '一个足够强的随机密码';
然后只授予这个网站真正需要的权限:
GRANT SELECT, INSERT, UPDATE, DELETE
ON mnewstv.*
TO 'mnewstv_app'@'localhost';
这样 PHP 程序连接数据库时使用:
$pdo = new PDO(
'mysql:host=localhost;dbname=mnewstv;charset=utf8mb4',
'mnewstv_app',
$password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]
);
即使网站存在 SQL Injection、文件读取漏洞或者配置文件泄露,攻击者首先获得的也是 mnewstv_app 的权限,而不是整个 MySQL 实例的管理员权限。
这里有一个非常重要的概念:MySQL 的 'user'@'host' 才是完整的账户身份。'web_user'@'localhost' 和 'web_user'@'%' 并不是同一个账户。MySQL 会同时根据用户名和连接来源判断账户身份。
因此,如果 PHP 和 MySQL 在同一台服务器上,通常没有必要写成:
CREATE USER 'web_user'@'%'
IDENTIFIED BY 'password';
更合理的是:
CREATE USER 'web_user'@'localhost'
IDENTIFIED BY 'password';
如果 PHP 应用服务器和 MySQL 是分开的,则可以进一步限制到应用服务器的固定地址,例如:
CREATE USER 'web_user'@'10.10.20.15'
IDENTIFIED BY 'password';
GRANT SELECT, INSERT, UPDATE, DELETE
ON mnewstv.*
TO 'web_user'@'10.10.20.15';
这里需要注意,限制 MySQL 账户的 host 只是第一层控制。数据库服务器的防火墙、安全组以及 MySQL 的网络监听配置也应该同时限制来源。不能因为账户写了一个 IP,就认为整个数据库网络已经安全。
更重要的是,不要为了“方便”直接:
GRANT ALL PRIVILEGES
ON *.*
TO 'web_user'@'%';
这实际上把一个网站账号变成了数据库管理员账号。应用程序通常不需要创建其他数据库用户,也不需要删除数据库、修改 MySQL 全局配置或者管理复制。因此,把管理员权限交给网站程序没有实际必要。
应用账号到底需要哪些权限,应当根据程序真正执行的 SQL 决定。
例如普通 SaaS 网站可能需要:
SELECT
INSERT
UPDATE
DELETE
如果程序需要执行数据库迁移,则可能还需要:
CREATE
ALTER
INDEX
DROP
但这里最好进一步区分运行账号和部署账号。
例如:
mnewstv_app
SELECT
INSERT
UPDATE
DELETE
mnewstv_migrate
CREATE
ALTER
INDEX
DROP
以及迁移工具真正需要的其他权限
mnewstv_admin
用于人工数据库管理
PHP 网站的生产进程只使用 mnewstv_app,而数据库结构升级由部署流程临时使用更高权限的账号完成。这样,即使网站运行环境被攻破,攻击者也不能直接利用应用账号修改整个数据库结构。
MySQL 还支持 Role,可以把一组权限集中管理。例如:
CREATE ROLE 'app_rw';
GRANT SELECT, INSERT, UPDATE, DELETE
ON mnewstv.*
TO 'app_rw';
GRANT 'app_rw'
TO 'mnewstv_app';
SET DEFAULT ROLE 'app_rw'
TO 'mnewstv_app';
Role 特别适合多个应用账号需要相同权限的环境。权限集中定义之后,可以减少逐个账号修改权限造成的遗漏。MySQL 官方也将 Role 作为权限集合管理机制。
权限审计时,不建议直接依赖查询 mysql.user 来判断某个账户最终到底拥有什么权限。MySQL 的权限可能来自全局、数据库、表、列以及角色等多个层级。最直接的检查方式是:
SHOW GRANTS FOR 'mnewstv_app'@'localhost';
如果使用 Role,也应该检查角色本身以及角色授予关系。MySQL 官方明确提供 SHOW GRANTS 用于查看账户的权限分配。
修改权限同样应该使用 GRANT 和 REVOKE,而不是直接修改 mysql 系统库中的授权表。MySQL 官方明确不建议直接通过 INSERT、UPDATE 或 DELETE 修改 grant tables。
例如发现网站实际上不需要删除数据,可以收回:
REVOKE DELETE
ON mnewstv.*
FROM 'mnewstv_app'@'localhost';
然后再次执行:
SHOW GRANTS FOR 'mnewstv_app'@'localhost';
确认最终权限。
数据库连接本身也需要考虑加密。如果 PHP 与 MySQL 在同一台服务器上使用 Unix socket,网络窃听风险与跨服务器 TCP 连接不同;但如果 PHP 应用和 MySQL 位于不同服务器、不同 VLAN 或云环境中,就应该认真考虑 TLS。
MySQL 可以通过:
SET PERSIST require_secure_transport = ON;
要求 TCP/IP 连接使用加密传输。需要注意的是,MySQL 官方对 require_secure_transport 的定义并不是“所有连接必须使用 TLS”,因为 Unix socket 和 Windows shared memory 也属于 secure transport。
对于特定账户,也可以在创建账户时设置连接加密要求。例如:
CREATE USER 'remote_app'@'10.10.20.15'
IDENTIFIED BY 'password'
REQUIRE SSL;
如果要求客户端提供有效的 X.509 证书,则可以使用:
REQUIRE X509;
这种方式适合对数据库连接安全要求更高的生产环境。
密码管理同样不能忽略。数据库密码不应该直接写进 Git 仓库,也不要把生产数据库密码硬编码到公开代码中。PHP 应用可以通过服务器环境变量、受控配置文件或者专门的 Secret 管理系统提供数据库凭据。
同时,不应该把“定期强制更换所有数据库密码”当成唯一的安全策略。对于生产系统,更重要的是随机、唯一、权限受限的凭据,以及在凭据泄露、人员离职、服务器迁移或者安全事件发生时能够快速轮换。
另外,数据库管理员账号和网站应用账号应该彻底分开。
例如:
root / DBA账号
数据库维护
创建用户
授予权限
数据库结构管理
mnewstv_migrate
数据库迁移
mnewstv_app
网站运行
mnewstv_readonly
报表、分析或只读工具
不要让 phpMyAdmin、MySQL Workbench 或日常开发工具长期保存 root 凭据,更不要为了方便让网站后台直接调用具有 CREATE USER、GRANT OPTION 等管理能力的账号。
尤其是 GRANT OPTION,它不是普通的数据读写权限。拥有它的账户可以把自己获得的权限进一步授予其他账户,因此对于网站运行账号通常没有理由授予。MySQL 官方的权限模型也明确区分普通对象权限与账户管理能力。
还有一个经常被忽略的问题:不要只保护数据库账号,却让 MySQL 本身暴露在整个互联网。
如果数据库只供 PHP 应用服务器访问,最理想的网络结构通常是:
Internet
↓
Web Server
↓
PHP Application
↓
Private Network
↓
MySQL
而不是:
Internet
↓
MySQL:3306
即使 MySQL 用户设置得非常严格,也没有必要把数据库端口直接暴露给公网。防火墙、安全组、监听地址和 MySQL 账户 host 限制应该形成多层防护。
最后,还应该定期检查到底有哪些账户存在:
SELECT user, host
FROM mysql.user
ORDER BY user, host;
然后针对具体账户:
SHOW GRANTS FOR 'mnewstv_app'@'localhost';
检查是否存在已经不用的账号、过于宽泛的 '%' host、拥有全局权限的应用账户,以及不再需要的管理员权限。
一个成熟的 PHP SaaS 数据库权限体系,并不是简单地把 root 改名成 web_user。真正有效的设计应该是让不同身份承担不同职责:网站只负责自己的数据,迁移账号负责结构变更,管理员负责数据库管理,报表账号负责读取数据。这样即使应用层出现漏洞,攻击者能够获得的权限也被限制在一个明确的范围内。
数据库安全的核心并不是让任何程序都“绝对不能出问题”,而是在程序、服务器或者凭据真的发生问题时,把爆炸半径限制在尽可能小的范围。对一个 PHP SaaS 系统来说,不使用 root 连接数据库只是第一步,真正成熟的权限设计应该继续落实到最小权限、独立账号、限定连接来源、角色管理、TLS、权限审计以及网络隔离。
MySQL 用户和权限怎么管理?不要让网站使用 root 账号连接数据库
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP