《PHP运行时安全基线体系:内核裁剪、禁用函数到容器沙箱的生产级安全标准》
PHP 运行时安全基线体系:从内核裁剪到容器沙箱 先用大白话讲清楚为什么要做、防的是什么,然后从编译期 → 配置期 → 运行期 → 容器期四层给一套完整的生产级标准,每层都有可直接抄的配置和解释。---〇、先用大白话讲清楚:这到底在防什么 PHP 是被攻击最多的语言之一,不是因为它烂,而是因为它用得多、功能太全。很多内置函数本来是给运维用的,落到攻击者手里就是凶器:-攻击者上传一个 shell.php,里面写system($_GET[cmd])→直接在你服务器上执行命令-一句eval($_POST[x])→把整个服务器变成攻击者的肉鸡-file_get_contents(/etc/passwd)→读你的系统文件、配置、密钥运行时安全基线一套默认就锁死的标准配置。核心思想是 纵深防御(Defense in Depth)—— 不指望某一道墙挡住所有人,而是设好几道墙,攻击者突破一层还有下一层:第1层 编译期裁剪 → 根本不编译进危险功能(攻击者想用都没有)第2层 配置期加固 → php.ini 禁用危险函数、限制文件访问 第3层 运行期防护 → RASP 实时拦截攻击行为 第4层 容器沙箱 → 就算 PHP 被攻破,也跳不出容器、提不了权 一句话总结基线哲学:默认拒绝(Deny bydefault),按需放开。 不是先全开,出事再关,而是先全关,确实要用才开。 下面四层逐一给。---第一层:编译期内核裁剪 最彻底的安全:危险的东西根本不编译进去。攻击者再厉害,也用不了一个不存在的函数。1.1从源码最小化编译 PHP #!/bin/bash#build-php-hardened.sh ——生产级最小化编译set-e PHP_VERSION8.3.8PREFIX/opt/php-hardenedcd/usr/local/src curl-fSLhttps://www.php.net/distributions/php-${PHP_VERSION}.tar.gz|tar xz cdphp-${PHP_VERSION}./configure \--prefix${PREFIX}\--enable-fpm \--with-fpm-userwww-data \--with-fpm-groupwww-data \ \ #只开生产真正需要的扩展 \--enable-opcache \--enable-mbstring \--with-openssl \--with-curl \--enable-pdo \--with-pdo-mysql \--with-zlib \--enable-sockets \ \ #明确关掉危险/不需要的 \--disable-cli \ # 生产 FPM 不需要 CLI,少一个攻击面 \--disable-phpdbg \--disable-debug \--without-pear \--disable-cgi \ \ #编译期硬化(让二进制本身更难被利用) \ CFLAGS-O2 -fstack-protector-strong -D_FORTIFY_SOURCE2 -fPIE\ LDFLAGS-Wl,-z,relro,-z,now -piemake-j$(nproc)make install 大白话:-./configure 的本质是勾选要哪些功能。默认很多扩展是不编译的,这正是我们要的 ——你不主动--with-xxx,它就不存在。---disable-cli:生产环境只跑FPM(处理网页请求),根本不需要命令行 PHP。少一个 php 命令,攻击者拿到 shell 也少一个利用工具。-CFLAGS/LDFLAGS 那串是编译期硬化:栈保护、地址随机化(防内存攻击)、只读重定位表。即使 PHP 本身有个内存漏洞,这些也让利用难度大增。这是工业级和能跑就行的区别。1.2扩展白名单原则 原则:能不装的扩展坚决不装。 绝对不要在生产装的(除非业务真需要):-phar →Phar 反序列化是经典攻击链-ffi →直接调用 C 函数,等于绕过所有 PHP 限制-posix →提供进程/用户操作,提权利用常用-pcntl →进程控制-shmop →共享内存操作 如果非装不可 →配合后面的 disable_functions 把危险函数单独关掉 大白话:每装一个扩展就多一批函数,多一批潜在攻击面。ffi 尤其危险——它能让PHP 直接调系统底层 C 函数,等于给攻击者开了后门,生产环境几乎永远不该开。---第二层:配置期加固 php.ini 编译进来的功能,用 ini 配置再锁一道。这是性价比最高的一层,改个配置就生效。2.1生产级安全 php.ini(可直接抄);;PHP 生产安全基线 php.ini;;---1)禁用危险函数(核心中的核心)---;命令执行类:这是 WebShell 的命脉,全部禁掉 disable_functionsexec,passthru,shell_exec,system,proc_open,popen,proc_close,proc_get_status,proc_terminate,# 代码执行/动态加载 \ dl,# 进程/系统信息 \ posix_getpwuid,posix_kill,posix_mkfifo,posix_setpgid,posix_setsid,posix_setuid,posix_uname,getmyuid,getmypid,# 文件/环境探测 \ pcntl_exec,putenv,# 危险的回调/反射利用(按业务评估) \ pcntl_fork,pcntl_signal;注意:eval、assert 是语言结构不是函数,disable_functions 关不掉;要靠后面的RASP(Snuffleupagus)或 assert.active0;---2)限制文件系统访问范围---;open_basedir:PHP 只能读写这几个目录,跳不出去;这一条能挡住90%的读 /etc/passwd遍历目录攻击 open_basedir/var/www/app:/tmp:/var/lib/php/sessions;---3)关闭信息泄露---expose_phpOff;不在 HTTP 头暴露 PHP 版本 display_errorsOff;生产绝不把错误显示给用户(泄露路径/代码)display_startup_errorsOff log_errorsOn;错误写日志,不给用户看 error_log/var/log/php/error.log;---4)远程文件远程代码执行的温床,全关---allow_url_fopenOff;禁止fopen(http://...)allow_url_includeOff;禁止 include 远程文件(RFI 攻击根除);---5)文件上传限制---file_uploadsOn upload_max_filesize8M max_file_uploads10upload_tmp_dir/var/lib/php/uploads;独立目录,后面容器里设 noexec;---6)Session 安全---session.cookie_httponly1;JS 读不到 cookie,防 XSS 偷 session session.cookie_secure1;只在 HTTPS 下传 cookie session.cookie_samesiteStrict;防 CSRF session.use_strict_mode1;防 session fixation 攻击;---7)资源限制(防 DoS/资源耗尽)---max_execution_time30max_input_time30memory_limit128M post_max_size8M max_input_vars1000;防哈希碰撞/参数轰炸;---8)其它---enable_dlOff;禁止运行时加载扩展 assert.activeOff;关掉assert(可被用来执行代码)逐条大白话:┌─────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐ │ 配置 │ 防什么 │ ├─────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤ │ │ 最重要。把 system/exec/shell_exec │ │ disable_functions │ 这些执行系统命令的函数全禁掉。攻击者就算传了 │ │ │ WebShell,也调不动命令,等于把刀拿走了。 │ ├─────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤ │ open_basedir │ 把 PHP 关进笼子,只能访问指定目录。它再想 cat/etc/passwd │ │ │ 就被拒绝。一条配置挡住绝大多数文件读取攻击。 │ ├─────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤ │ allow_url_includeOff │ 根除RFI(远程文件包含):攻击者没法让你include(http://恶意网站/shell.txt)。 │ ├─────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤ │ expose_php/display_errors │ 不给攻击者送情报。报错信息里常含绝对路径、SQL、代码片段,关掉别让用户看见。 │ ├─────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤ │ cookie_httponly/secure/samesite │ 保护用户 session,防 XSS 偷 cookie、防 CSRF。 │ ├─────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤ │ 资源限制 │ 防止单个请求把 CPU/内存吃光,拖垮整台机器。 │ └─────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘ ▎ 关键提醒:eval()、assert()是语言结构不是函数,disable_functions 管不了它们。要拦 eval,得靠第三层的 ▎ RASP。这是很多人踩的坑。2.2PHP-FPM 进程加固 www.conf;/opt/php-hardened/etc/php-fpm.d/www.conf;---用低权限用户跑,绝不用 root---userwww-data groupwww-data;---监听本地 socket,不暴露端口---listen/run/php/php-fpm.sock listen.ownerwww-data listen.groupwww-data listen.mode0660;---进程级open_basedir(双保险)---php_admin_value[open_basedir]/var/www/app:/tmp;用 php_admin_value 而非 php_value:应用代码无法用 ini_set 覆盖它!;---禁止应用代码改危险配置---php_admin_value[disable_functions]exec,system,shell_exec,passthru,proc_open,popen php_admin_flag[allow_url_fopen]off;---限制每个进程内存,防泄漏拖垮系统---php_admin_value[memory_limit]128M;---慢日志:抓出异常慢的请求(可能是攻击)---slowlog/var/log/php/slow.log request_slowlog_timeout5s;---进程数限制,防 fork 炸弹---pmdynamic pm.max_children50pm.max_requests500;跑500个请求后重启进程,防内存泄漏累积 大白话:-php_admin_value vs php_value 的区别是致命的:用 php_admin_value 设的,应用代码没法用ini_set()改回来。安全配置一定要用 admin 版本,否则攻击者一句ini_set(open_basedir,/)就破了你的防线。-FPM 必须用 www-data 这种低权限用户跑。绝不能用 root,否则 PHP 一旦被攻破,攻击者直接拿到 root。-pm.max_requests500:进程处理一定数量请求后自动重启,防止内存泄漏和某些驻留型攻击。---第三层:运行期防护(RASP)前两层是静态的规则。但有些攻击是动态的(比如 eval 执行、SQL 注入)。RASP(运行时应用自我保护)嵌在 PHP 内部,实时看每个函数调用,发现可疑行为就拦。 主流方案:Snuffleupagus(开源,生产可用)。3.1安装 Snuffleupagus # 编译安装(它本身是个 PHP 扩展)git clone https://github.com/jvoisin/snuffleupaguscd snuffleupagus/src phpize./configure--enable-snuffleupagusmakemake install#php.ini 启用echoextensionsnuffleupagus.sophp.ini echosp.configuration_file/etc/php/snuffleupagus.rulesphp.ini3.2规则配置 snuffleupagus.rules #把 eval/assert 这些禁不掉的也管起来# 禁止 system 类函数接收来自请求的参数(命令注入防护)sp.disable_function.function(system).drop();sp.disable_function.function(exec).drop();sp.disable_function.function(shell_exec).drop();# 只允许特定文件调用eval(几乎等于全禁,极大削弱 WebShell)sp.eval_whitelist.list(base64_decode,gzinflate).simulation();# 防止上传的文件被当 PHP 执行(WebShell 上传防护核心)sp.upload_validation.script(/opt/scripts/upload_check.sh).enable();# 给所有 cookie 自动加密绑定到环境,防篡改 sp.cookie_encryption.name(session).enable();# 禁止读取敏感文件 sp.readonly_exec.enable();# 禁止执行可写目录里的PHP(防上传执行)大白话:-Snuffleupagus 能做 disable_functions 做不到的事:比如只在参数来自用户输入时才拦system(精准,不误伤合法用法),或者只允许白名单文件用 eval。-readonly_exec:禁止执行可写目录里的 PHP 文件。这是 WebShell 的克星——攻击者上传的shell 通常在 uploads/这种可写目录,这条规则让它根本跑不起来。-upload_validation:文件上传时自动扫描,发现是伪装的 PHP 就拒绝。---第四层:容器沙箱(最外层兜底)前三层都是让 PHP 内部更安全。第四层的思路不同:假设 PHP 已经被攻破了,怎么把损失关在容器里,不让它祸害宿主机和其它服务?4.1安全的 Dockerfile # 多阶段构建:编译和运行分离,最终镜像不含编译工具 FROM debian:12-slim AS runtime #1)创建低权限用户(绝不用 root 跑应用)RUN groupadd-r appuseruseradd-r-g appuser-s/usr/sbin/nologin appuser #2)拷贝已编译好的硬化版 PHP COPY--frombuilder/opt/php-hardened/opt/php-hardened COPY--chownappuser:appuser./app/var/www/app #3)删掉容器里所有 shell 和危险工具(攻击者拿到 RCE 也没工具用)RUN rm-f/bin/sh/bin/bash/bin/dash \rm-f/usr/bin/wget/usr/bin/curl/usr/bin/nc \rm-f/bin/cat/bin/chmod/usr/bin/find #4)切到低权限用户 USER appuser WORKDIR/var/www/app EXPOSE9000ENTRYPOINT[/opt/php-hardened/sbin/php-fpm,--nodaemonize]4.2运行时安全约束(docker-compose/k8s)#docker-compose.yml ——生产级安全约束services:php-app:image:php-hardened:8.3user:1000:1000# 非 root 运行 read_only:true# ★根文件系统只读!攻击者写不进任何文件 tmpfs:# 只有这几个目录可写(且在内存里)-/tmp:size64M,mode1777,noexec # noexec:写进去的文件不能执行-/var/lib/php/sessions:size32M,noexec-/run/php cap_drop:-ALL # ★丢弃所有 Linux 能力 cap_add:[]# 一个都不加(PHP-FPM 不需要任何特权能力)security_opt:-no-new-privileges:true# ★禁止提权(setuid 等失效)-seccomp:/etc/docker/php-seccomp.json # 系统调用白名单 pids_limit:100# 防 fork 炸弹 mem_limit:256m # 内存上限 cpus:1.0networks:-internal # 只接内部网,不直接暴露公网 networks:internal:internal:true# 这个网段访问不了外网(防数据外传/反弹shell)逐条大白话(这层是精华):┌────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────┐ │ 约束 │ 防什么 │ ├────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤ │ read_only:true│ 根文件系统只读。攻击者就算有了 RCE,也写不进任何文件——上传不了 │ │ │ WebShell、改不了配置、植入不了后门。这是最强的一招。 │ ├────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤ │ tmpfs...noexec │ 必须可写的目录(临时文件、session)放内存里,且标记 noexec:写进去的文件不许执行。攻击者把 │ │ │ shell 写到/tmp 也跑不起来。 │ ├────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤ │ cap_drop:ALL │ 丢弃所有 Linux 特权能力。PHP 处理网页不需要任何特权,全丢掉后攻击者提权的路被堵死。 │ ├────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤ │ no-new-privileges │ 禁止进程获得比父进程更高的权限,setuid 类提权直接失效。 │ ├────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤ │ seccomp │ 只允许 PHP 真正用到的系统调用,封掉 ptrace、mount 等危险调用(逃逸常用)。 │ ├────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤ │ 内部网络 internal │ 容器连不上外网。攻击者就算拿到 │ │ │ shell,也没法反弹连接出去、没法下载第二阶段工具、没法把你的数据传走。 │ ├────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤ │ 删掉 │ 容器里没有 sh、curl、wget。攻击者拿到 RCE 想 curl 恶意服务器|sh 都没工具,大幅削弱利用。 │ │ shell/curl/wget │ │ └────────────────────┴────────────────────────────────────────────────────────────────────────────────────────────┘4.3seccomp 白名单(片段){defaultAction:SCMP_ACT_ERRNO,syscalls:[{names:[read,write,open,close,stat,mmap,socket,connect,accept,epoll_wait,futex,brk],action:SCMP_ACT_ALLOW}]}大白话:defaultAction:ERRNO默认拒绝所有系统调用,只放行白名单里的。容器逃逸经常依赖 mount、ptrace、unshare 这类系统调用,不在白名单里就直接被内核拒绝。又是默认拒绝哲学。---五、完整防御层级总览 把四层串起来看一次攻击是怎么被层层拦截的:攻击者上传 shell.php(含system($_GET[c]))并访问 │ ├─ 第4层:uploads 目录 noexec →shell 根本跑不起来 ❌ 拦截 ├─ 第3层:Snuffleupagus readonly_exec →可写目录的PHP禁止执行 ❌ 拦截 ├─ 第2层:disable_functions →system 被禁,调不动 ❌ 拦截 ├─ 第1层:编译时没装 posix/ffi →想提权也没工具 ❌ 拦截 └─ 就算全突破了:read_only 根文件系统 →写不了后门 ❌ 拦截 internal 网络 →数据传不出去、shell 弹不出来 ❌ 拦截 cap_drop ALL →提不了权,逃不出容器 ❌ 拦截 这就是纵深防御的威力:任何单层都可能被绕过,但要同时突破四层几乎不可能。---六、生产安全基线 Checklist ┌──────┬─────────────────────────┬────────────────────────────────────────────────────────────┐ │ 层 │ 检查项 │ 标准 │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ 编译 │ 最小化扩展 │ 只编译业务必需的;禁 ffi/phar/posix │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ 编译硬化 │-fstack-protector-strong-D_FORTIFY_SOURCE2relro,now pie │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ 禁用 CLI/CGI │ 生产 FPM 不要 CLI │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ 配置 │ disable_functions │ 命令执行类全禁 │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ open_basedir │ 限定到应用目录 │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ allow_url_fopen/include │ 全 Off │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ display_errors │ Off,错误只进日志 │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ expose_php │ Off │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ php_admin_value │ 安全项用 admin,禁止应用覆盖 │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ Session cookie │ httponlysecuresamesiteStrict │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ 运行 │ RASP │ 部署 Snuffleupagus,启用 readonly_exec │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ 上传校验 │ 拦伪装 PHP │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ 容器 │ 非 root 运行 │ user 非0│ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ 只读根文件系统 │ read_only:true│ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ 可写目录 noexec │ tmpfsnoexec │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ cap_drop ALL │ 不加任何 capability │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ no-new-privileges │true│ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ seccomp │ 默认拒绝白名单 │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ 网络隔离 │ 内部网,不直连公网 │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ 资源限制 │ pids/mem/cpu 都设上限 │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ 持续 │ 镜像扫描 │ Trivy/Grype 扫 CVE │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ 配置审计 │ 定期跑基线检查脚本 │ ├──────┼─────────────────────────┼────────────────────────────────────────────────────────────┤ │ │ 日志监控 │ 错误日志慢日志异常调用告警 │ └──────┴─────────────────────────┴────────────────────────────────────────────────────────────┘---七、落地优先级(别一次全上)按投入产出比排序,先做前三步就能挡住绝大多数攻击:1.立刻做(改配置,零成本):disable_functionsopen_basedirallow_url_includeOffdisplay_errorsOff。一个下午搞定,挡住80%的常见攻击。2.本周做:FPM 用低权限用户php_admin_value 锁死配置Session cookie 加固。3.本月做:容器化,上 read_onlycap_drop ALL非 root网络隔离。这层一上,即使被 RCE 也很难造成实质破坏。4.持续优化:RASP(Snuffleupagus)seccomp 精调最小化重编译镜像 CVE 扫描。---核心哲学再强调一遍:默认拒绝,纵深防御,假设已被攻破。 不要把安全压在某一道墙上,而是让攻击者每突破一层都还面对下一层——直到他放弃。 要不要我针对某层给更深的实战内容?比如:1.完整的基线自动化检查脚本(一键扫描 php.ini/容器是否合规)2.Snuffleupagus 完整规则集(覆盖反序列化、SQL注入、XXE)3.Kubernetes 下的 PodSecurityNetworkPolicy 完整 PHP 部署清单4.WebShell 检测文件完整性监控(被植入后门的实时告警)告诉我方向,我展开成可直接部署的工程代码。