本文目录导读:

- 数据库连接:使用权限最小的数据库用户
- 文件系统权限:限制 PHP 进程的读写范围
- 会话与临时文件:隔离临时目录
- 系统命令执行:白名单 + 禁用危险函数
- 权限检查:每次操作前验证
- 错误信息最小化
- Composer 依赖的权限隔离
- PHP-FPM 进程池隔离(多站点)
- 总结检查清单
在 PHP 中实现 最小特权原则(Principle of Least Privilege,PoLP) 的核心思想是:只给代码、用户、进程恰好够用的权限,既不更多,也不更少。
以下是针对 PHP 开发中贯彻最小特权的具体策略和代码实践:
数据库连接:使用权限最小的数据库用户
常见错误:整个项目用一个 root 用户连接数据库。
正确做法:为不同模块创建不同权限的数据库用户。
// 只读模块(如首页展示)使用只读账号
$readonly_db = new PDO('mysql:host=localhost;dbname=blog', 'blog_reader', 'password1', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
// 写入模块(如发布文章)使用增删改账号
$write_db = new PDO('mysql:host=localhost;dbname=blog', 'blog_writer', 'password2', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
SQL 层面配置:
-- 只读权限 GRANT SELECT ON blog.* TO 'blog_reader'@'localhost'; -- 增删改权限(不给 DROP、ALTER) GRANT SELECT, INSERT, UPDATE, DELETE ON blog.* TO 'blog_writer'@'localhost';
文件系统权限:限制 PHP 进程的读写范围
原则:PHP 进程(通常是 www-data 或 nobody)只能访问它必须访问的目录。
目录权限配置(Linux/Unix):
# 网站根目录(PHP 可读,不可写) chmod 755 /var/www/html chown root:www-data /var/www/html # 上传目录(PHP 可读写,但禁止执行PHP) chmod 755 /var/www/html/uploads chown www-data:www-data /var/www/html/uploads # 配置文件(PHP 可读,绝对不可写) chmod 640 /var/www/html/config.php chown root:www-data /var/www/html/config.php # 日志目录(PHP 只写,不可读已存在的日志) chmod 730 /var/log/myapp/ chown www-data:www-data /var/log/myapp/
禁用文件执行权限(防止上传 PHP 马): 在 Nginx/Apache 配置中对上传目录取消 PHP 执行:
location /uploads/ {
location ~ \.php$ {
deny all;
}
}
会话与临时文件:隔离临时目录
常见错误:PHP 默认临时目录 /tmp 可能被其他用户读取。
改进:使用项目私有临时目录,并设置严格权限。
// 在入口文件设置
session_save_path('/var/www/project/tmp/sessions');
ini_set('upload_tmp_dir', '/var/www/project/tmp/uploads');
// 确保目录权限
// chmod 700 /var/www/project/tmp/sessions
// chown www-data:www-data /var/www/project/tmp/sessions
系统命令执行:白名单 + 禁用危险函数
原则:除非必须,不使用 exec()、shell_exec()、system(),如果必须使用,限制输入范围。
// ❌ 糟糕:直接拼接用户输入
exec("convert {$_GET['img']} output.png");
// ✅ 较好:白名单 + 参数化
$allowed_commands = [
'resize' => 'convert %s -resize 200x200 output.png',
'compress' => 'convert %s -quality 80 output.jpg'
];
$action = $_GET['action'] ?? '';
if (!array_key_exists($action, $allowed_commands)) {
die('Invalid command');
}
$file = basename($_GET['file']); // 去掉路径
$command = sprintf($allowed_commands[$action], escapeshellarg($file));
exec($command, $output, $return_code);
禁用高危函数(php.ini 或 disable_functions):
disable_functions = exec, shell_exec, system, passthru, popen, proc_open, pcntl_exec
权限检查:每次操作前验证
最小特权不仅体现在系统层面,也体现在业务逻辑。
// 用户只能修改自己的文章
class ArticleController {
public function update(int $articleId, array $data) {
$article = Article::find($articleId);
// ⭐ 权限检查:不是用户自己的文章,也不是管理员,则拒绝
if ($article->user_id !== Auth::id() && !Auth::user()->isAdmin()) {
http_response_code(403);
die('Insufficient permissions');
}
$article->update($data);
}
}
错误信息最小化
原则:生产环境不暴露任何敏感信息。
// php.ini 配置
display_errors = Off
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
// 使用自定义错误处理
set_error_handler(function($severity, $message, $file, $line) {
// 记录到日志,但返回通用错误
error_log("Error: $message in $file:$line");
http_response_code(500);
echo 'An internal error occurred.';
});
Composer 依赖的权限隔离
问题:Composer 的 vendor 目录如果可写,可能被攻击者修改依赖。
解决方案:
# 部署后锁定 vendor 目录 chmod -R 755 vendor # 或者使用 composer 的 --no-dev 并禁用自动更新 composer install --no-dev --optimize-autoloader composer config --global disable-tls false
PHP-FPM 进程池隔离(多站点)
如果使用 PHP-FPM,可以为不同站点创建独立的进程池,并以不同用户运行:
; /etc/php/8.x/fpm/pool.d/site1.conf [site1] user = site1_user group = site1_user listen = /run/php/php8.x-fpm-site1.sock ; /etc/php/8.x/fpm/pool.d/site2.conf [site2] user = site2_user group = site2_user listen = /run/php/php8.x-fpm-site2.sock
总结检查清单
| 场景 | 最小特权做法 | 风险点 |
|---|---|---|
| 数据库 | 使用专用只读/写入账号 | 使用root账户 |
| 文件系统 | 目录755/750,文件644/640 | 使用777 |
| 临时文件 | 私有temp目录,session独立 | 使用系统/tmp |
| 命令执行 | 白名单+escapeshellarg | 直接拼接用户输入 |
| 错误处理 | 日志详细,输出模糊 | 生产环境显示错误详情 |
| PHP扩展 | 只加载需要的扩展 | 加载不必要扩展增加攻击面 |
核心思想:每次授予一个权限时,问自己:“如果这个权限被滥用,最坏的结果是什么?” 只给那些即使被滥用也不会造成严重后果的权限。