PHP网站连接MySQL时,最容易被忽略的安全问题之一,就是数据库账号到底拥有多大的权限。很多网站为了省事直接使用root,或者给应用账号授予ALL PRIVILEGES。这样做在开发环境可能感觉方便,但一旦网站程序、插件或者接口出现SQL注入或其他漏洞,攻击者拿到的就不只是某张表的权限,而可能进一步获得整个数据库甚至更多服务器级能力。
MySQL的GRANT机制真正解决的问题,就是把“网站需要做什么”和“数据库允许做什么”分开。对于PHP应用来说,通常应该建立独立的应用账号,只授予程序运行过程中真正需要的权限。
最常见的权限包括SELECT、INSERT、UPDATE和DELETE。SELECT用于读取数据,INSERT用于新增记录,UPDATE用于修改记录,DELETE用于删除记录。CREATE、ALTER、DROP等DDL权限则涉及数据库结构变化,生产环境中的普通PHP运行账号通常没有必要拥有这些权限。
例如一个网站运行时只需要读取、增加和修改业务数据,可以建立这样的账号:
CREATE USER 'php_user'@'localhost'
IDENTIFIED BY '使用密码管理器生成的强密码';
GRANT SELECT, INSERT, UPDATE
ON dbname.*
TO 'php_user'@'localhost';
这里的localhost非常重要。它表示这个账号只允许从本机连接。如果PHP和MySQL运行在同一台服务器上,通常没有必要为了方便而写成'%'。'%'代表更宽泛的主机匹配范围,会扩大数据库账号可以尝试连接的位置。
如果网站还需要删除业务记录,可以增加DELETE:
GRANT SELECT, INSERT, UPDATE, DELETE
ON dbname.*
TO 'php_user'@'localhost';
而CREATE、ALTER、DROP等权限最好与网站运行账号分开。比如数据库迁移、表结构升级由部署账号或者维护人员执行,而PHP运行账号只负责正常业务操作。这样即使网站程序被攻破,攻击者能够利用的数据库权限也会受到限制。
GRANT也可以精确到单张表。例如网站的统计账号只需要读取orders表:
GRANT SELECT
ON dbname.orders
TO 'report_user'@'localhost';
如果不同账号负责不同功能,还可以进一步限制权限。例如后台程序可以修改内容表,而报表账号只能读取数据。权限越接近实际业务需求,发生应用漏洞以后造成的影响范围通常越容易控制。
MySQL还支持列级权限。例如某个账号只需要读取用户姓名和注册时间:
GRANT SELECT (username, created_at)
ON dbname.users
TO 'report_user'@'localhost';
不过列级权限会增加权限管理复杂度。对于大多数普通PHP网站来说,先做好数据库、表和账号之间的隔离,往往比把每一个字段都拆成权限更加实际。
另一个容易流传的错误,是认为执行GRANT以后必须手动执行FLUSH PRIVILEGES。对于正常使用CREATE USER、GRANT、REVOKE等SQL语句修改权限的情况,并不需要额外执行这个命令。MySQL会立即处理这些权限变化。FLUSH PRIVILEGES主要用于权限表被直接修改等特殊场景,因此不应该作为每一次GRANT后的固定步骤。
查看权限时,也不要直接查询mysql.user然后寻找一个所谓的privileges字段。更可靠的方法是:
SHOW GRANTS FOR 'php_user'@'localhost';
这个命令能够直接显示该账号实际获得的授权。例如:
GRANT SELECT, INSERT, UPDATE
ON `dbname`.* TO `php_user`@`localhost`
如果需要检查当前用户,可以使用:
SELECT CURRENT_USER();
对于生产环境的权限审计,还应该把数据库授权和应用账号、服务器账号分别考虑。数据库用户并不等于Linux用户,PHP-FPM运行账户也不等于MySQL账户。每一层都应该只拥有完成自身工作的权限。
密码方面,也不要把“12位、大小写、数字、特殊字符、每三个月更换一次”当成MySQL统一规定。实际要求取决于MySQL服务器配置以及组织自己的安全政策。更重要的是不要在源代码、Git仓库或者公开配置文件中硬编码生产数据库密码。
PHP代码可以通过环境变量或受保护的配置文件取得数据库凭据。例如使用PDO:
$host = 'localhost';
$dbname = 'dbname';
$user = 'php_user';
$pass = getenv('DB_PASSWORD');
$pdo = new PDO(
"mysql:host=$host;dbname=$dbname;charset=utf8mb4",
$user,
$pass,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false
]
);
这里还需要区分“数据库权限”和“SQL注入防护”。即使PHP账号只有SELECT、INSERT、UPDATE权限,程序仍然可能因为SQL注入产生严重问题。因此数据库权限只是最后一道限制,SQL参数绑定、输入验证和正确的授权检查仍然必须存在。
如果PHP和MySQL不在同一台服务器上,还需要处理网络访问问题。这时不能简单地把账号改成'php_user'@'%'然后打开3306端口。更合理的做法是限制允许连接数据库的服务器地址,并结合防火墙、安全组以及MySQL账户的host限制控制访问来源。如果环境支持TLS,还应该配置MySQL客户端与服务器之间的加密连接,并验证服务器证书,而不是仅仅打开一个所谓的SSL选项。
MySQL 8.x还支持角色。角色适合多个账号共享一套权限。例如可以创建一个只读角色:
CREATE ROLE 'app_reader';
GRANT SELECT
ON dbname.*
TO 'app_reader';
CREATE USER 'php_report'@'localhost'
IDENTIFIED BY '使用密码管理器生成的强密码';
GRANT 'app_reader'
TO 'php_report'@'localhost';
这样以后需要调整报表账号权限时,可以修改角色,而不必逐个修改大量用户。对于大型SaaS或者多应用环境,这种管理方式会更加清晰。
如果网站同时存在开发、测试和生产环境,也最好不要让三个环境共享同一个数据库账号。开发环境账号、测试环境账号和生产环境账号应该分别管理。更进一步,还可以让迁移工具使用拥有DDL权限的独立账号,而PHP-FPM使用没有CREATE、ALTER、DROP权限的运行账号。
生产环境还需要考虑数据库连接数量。max_connections = 100并不是一个通用的最佳值。PHP-FPM可能同时运行多个worker,如果每个worker都能够建立数据库连接,PHP并发量与MySQL连接数之间就会形成直接关系。数据库服务器的内存、查询持续时间、连接建立方式以及应用架构都需要一起计算。简单把max_connections设置成一个漂亮的整数,反而可能让数据库在高峰期耗尽内存。
PHP中的PDO也不能简单等同于传统数据库连接池。PHP-FPM环境通常需要根据实际部署方式考虑普通连接、持久连接、外部连接池或者数据库代理。真正重要的是控制连接数量、及时释放连接以及缩短长时间运行的SQL,而不是为了“使用连接池”三个字而增加复杂组件。
对于权限较大的账号,还应该定期执行:
SHOW GRANTS FOR 'php_user'@'localhost';
检查账号是否出现不必要的权限。特别是网站经过多年升级以后,数据库账号经常会积累历史权限。一个最初只需要SELECT和INSERT的账号,几年以后可能已经被开发人员临时增加了UPDATE、DELETE、CREATE甚至更高权限,却从来没有回收。
数据库权限设计还有一个非常重要的原则:不要把权限控制当成业务授权。比如PHP账号拥有UPDATE权限,并不意味着当前登录用户就应该能够修改所有用户的数据。数据库权限解决的是“应用程序能不能执行UPDATE”,而PHP业务逻辑解决的是“这个用户有没有权修改这一条记录”。SaaS系统中的tenant_id隔离、管理员权限和普通用户权限仍然必须在应用层严格检查。
因此,一个比较稳妥的PHP网站数据库权限结构通常是:生产环境使用独立MySQL账号,不使用root运行网站;账号限制连接来源;PHP运行账号只拥有业务所需的SELECT、INSERT、UPDATE、DELETE等权限;DDL操作使用独立维护账号;不同应用和环境使用不同账号;数据库连接使用安全的凭据管理方式;跨服务器连接时限制网络来源并根据需要启用TLS;定期使用SHOW GRANTS审查授权。
GRANT真正有价值的地方,并不是让数据库管理员记住一长串SQL语法,而是建立一道清晰的权限边界。网站程序应该能够完成自己的工作,却不应该因为程序出现漏洞,就自动拥有修改数据库结构、访问其他应用数据或者执行不必要操作的能力。对于PHP网站来说,最小权限不是一项额外功能,而应该从数据库账号创建的第一天就成为架构的一部分。
MySQL GRANT 怎么使用?给 PHP 网站创建合适的数据库访问权限
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP