禁用目录PHP执行权限:5种服务器加固方案

PHP类CMS被挂马的核心入侵路径是任意文件上传加目录可执行的组合拳。本文给出.htaccess、Apache vhost、Nginx、IIS、宝塔面板五条主流加固方案,覆盖uploads与data与templets与cache等典型目录的php、phtml、phar全套扩展名拦截,含双扩展名兜底、五个真实踩坑记录与命令行验证流程。

更新 21 分钟阅读 1,358 阅读

PHP类CMS被挂马的最常见入侵路径不是程序漏洞本身,而是任意文件上传加目录可执行权限的组合拳。攻击者通过头像上传、富文本编辑器、第三方插件把伪装成图片的脚本文件丢进uploads目录,由于该目录默认拥有PHP解析权限,访问shell.php?cmd=ls就能直接拿到webshell,顺着提权、横向、植入挖矿或暗链整站沦陷。本文我把2018年帮客户处理被挂马DedeCMS站点之后整理的硬化方案梳理一遍,2025年又重新校对,给出.htaccess、Apache vhost、Nginx、IIS、宝塔面板五条主流路径的完整配置,几行规则就能堵住一类高危后门。

为什么必须禁用特定目录的PHP执行权限

整套攻击链的关键节点是目录可执行。只要上传目录不能跑PHP,绝大多数webshell直接哑火,攻击者要么换成更难写的纯HTML跳板(成功率极低),要么换成内存马(需要更高权限)。这是所有Web应用安全里性价比最高的硬化措施之一,部署只要5分钟。

DedeCMS后台首页那条uploads加data目录有PHP执行权限的红色警告,本质就是在提示这件事。我维护过的几台老站光是把这条规则铺上去,WAF日志里webshell类payload的成功率从月均3到5起降到0。在我接触过的大约二十次站点入侵复盘里,有十六次的入口都是上传目录可执行加文件类型校验绕过。

常见绕过文件类型校验的手段有四类:第一是把php改成phtml、php3、php5、php7、phar等同样能被PHP-FPM解析的扩展名;第二是双扩展名如shell.php.jpg利用Apache的mod_mime还原;第三是利用%00截断(PHP 5.3之前);第四是利用文件头伪造(前几个字节是JPEG但后面是PHP代码)。任何一种绕过都意味着上传过滤不可信,必须靠服务器层禁止执行兜底。

动手前的环境确认与目录清单

不同的服务器架构对应不同的实现方式,先把环境搞清楚再动手:

  • 共享虚拟主机:通常只能用.htaccess,必须确认主机商开启了AllowOverride All和mod_rewrite。
  • 独立Apache服务器:推荐写在httpd.conf或vhost.conf里,效率比.htaccess高,且不会被攻击者改文件绕过。
  • Nginx:在server块里用location指令控制。
  • Windows加IIS:在IIS管理器里去掉目录的脚本执行权限,或在web.config里写handler规则。
  • 宝塔面板或1Panel:在站点配置文件里直接编辑Nginx或Apache配置段。

需要禁用PHP执行的典型目录清单:用户上传类(uploads、attachments、usr/uploads)、缓存与数据类(data、cache、runtime、tmp)、模板源文件类(templets、templates,仅DedeCMS这类把模板直接include的CMS需要例外保留)、静态资源类(static、assets、css、js、images)、编辑器目录类(editor、kindeditor/upload、ueditor/php/upload)。

风险提示:禁用前务必确认该目录里没有合法运行的PHP入口。我曾见过有人把templets全锁死,结果DedeCMS部分动态调用直接报500错误。建议先在测试环境跑一遍再推到生产,且部署完之后立刻执行curl验证。

方案一:.htaccess加RewriteRule(共享主机首选)

新建.htaccess文件(注意是以点开头、没有扩展名),编码选UTF-8无BOM或ANSI(Windows记事本另存时选所有文件,避免被加上.txt后缀)。把以下规则贴到根目录的.htaccess里。

规则的核心结构是:先用RewriteEngine On开启重写,然后用RewriteCond判断REQUEST_FILENAME是否是真实存在的文件(避免对不存在的路径浪费资源),再用RewriteRule针对uploads、data、templets、cache四个目录下的php、php3、php4、php5、php7、phtml、pht、phar等扩展名一律返回403 Forbidden。最后加一条兜底规则,把任何带双扩展名(如shell.php.jpg)的文件全部拦截。

关键标志位:F表示直接返回403禁止访问;NC表示扩展名不区分大小写防.PHP、.Php绕过;L表示匹配后停止后续规则。我在原始版本上加了三处实战增强:第一是把后缀从php扩展到php、php3、php4、php5、php7、phtml、pht、phar全套;第二是加上双扩展兜底(shell.php.jpg在某些Apache mod_mime配置下仍会被当PHP跑);第三是加了RewriteCond判断REQUEST_FILENAME是否真实存在。

验证方法:上传一个test.php到uploads目录(内容是简单的phpinfo调用),浏览器访问应返回403而不是显示phpinfo页面。

方案二:Apache独立服务器在vhost里硬规则

.htaccess有两个缺点:每次请求都要重新解析(性能开销)、攻击者拿到webshell后可以直接修改它(防御失效)。独立服务器一定要写在主配置里,并把AllowOverride None关掉对应目录的htaccess覆盖。

VirtualHost块里针对每个需要禁用的目录用Directory块包裹,里面用FilesMatch指令匹配php、php3、php4、php5、php7、phtml、pht、phar、asp、aspx、jsp等所有可执行扩展名,然后用Require all denied直接拒绝访问。再叠加php_admin_flag engine off彻底关闭这个目录的PHP引擎,是双保险。

注意点:Apache 2.4用Require all denied,2.2才用老语法Order deny allow配合Deny from all,如果你跑的是CentOS 7自带的httpd 2.4一定别抄旧文档。php_admin_flag engine off要求mod_php模式,PHP-FPM模式下这个指令无效,要改用Nginx方案或在FPM层配置。配置完执行apachectl configtest检查语法,再systemctl reload httpd平滑重载。

方案三:Nginx写法(2025年的主流)

很多老教程只讲Apache,但实际上Nginx加PHP-FPM已经是中文站长的主流。在server块里加一个正则location,匹配uploads、data、templets、cache、tmp任一目录下的php、php3、php5、php7、phtml、pht、phar扩展名,动作是deny all加return 403。再叠加一个针对双扩展名的拦截location。最后才是通用的php处理location(fastcgi_pass到PHP-FPM)。

关键经验:禁用location必须写在通用php location之前。Nginx的正则location匹配是顺序的,写反了规则不生效。我帮人查过一次配置看着对但攻击仍然成功的诡异问题,最后就是这个顺序坑——禁用规则被写在了php处理规则之后,Nginx匹配到php处理就停了,禁用规则永远不执行。

另一个常见坑是location的修饰符。波浪号(~)表示区分大小写的正则匹配,星号波浪号(~*)表示不区分大小写。强烈建议用~*版本,否则攻击者用大写PHP扩展名能绕过。配完后nginx -t检查,再nginx -s reload平滑重载。

对于使用OpenResty或Tengine的站点,配置语法完全相同。但要注意Tengine 2.x自带的concat模块可能会把多个PHP文件合并响应,绕过我们的禁用规则,建议禁用concat或在禁用规则前加一条Tengine专用的拦截。

方案四:Windows IIS 7、8、10配置

Windows主机的话有两条路:图形界面方式与web.config方式。

图形界面(最快):打开IIS管理器定位到站点下的uploads、data、静态生成目录,双击中间面板的处理程序映射,右侧选编辑功能权限,取消勾选脚本和执行只保留读取,应用即可。这种方式直接降级目录的执行策略,比逐个扩展名禁用更彻底。

web.config(推荐写入版本管理):在对应目录下放一个web.config,根节点是configuration、子节点是system.webServer,里面用handlers的accessPolicy属性设为Read降级目录访问策略,再用security里的requestFiltering加fileExtensions子节点逐个声明php、phtml、asp、aspx的allowed属性为false。这种方式好处是配置文件可以纳入Git版本管理,部署时跟着代码一起走。

IIS与Apache、Nginx的最大区别是IIS的处理程序映射可以在目录级别独立配置,不需要全局规则。但坑是子目录会自动继承父目录的处理程序映射,如果父目录有php处理映射,子目录的拒绝规则不一定生效,需要在子目录的web.config里显式removeAll或remove掉特定handler。

方案五:宝塔面板与1Panel一键操作

如果你用的是宝塔面板(很多中文站长在用),路径是网站、设置、配置文件,把上面Nginx方案的location段贴在server块里保存即可。宝塔会自动调用nginx -t验证语法。修改完保存时如果报语法错误,宝塔会提示错误位置,按提示改完再保存。

1Panel类似:网站、站点、配置文件,注意它的模板继承机制,别被全局模板覆盖了。1Panel在2024年之后引入了Nginx配置模板继承功能,全局模板会覆盖单站配置,建议在全局模板里就把禁用规则加进去,这样新建站点自动继承不需要每个站手动配。

宝塔的另一个优势是网站防火墙模块自带文件类型禁止执行选项,不需要手动写location规则,勾选即可。但这个功能仅企业版(每年299元起)支持,免费版要手动写规则。

五个真实踩坑记录

坑1:禁用规则被双重路径绕过。某次客户站用了Apache的mod_alias做了路径别名,把/files/映射到/uploads/,攻击者直接访问/files/shell.php绕过了我们对/uploads/的禁用规则。修复方法是用FilesMatch加SetHandler None作用于物理目录而不是URL路径,或者用Apache的Location指令配合RegexLocation同时匹配两个URL前缀。

坑2:PHP-FPM的cgi.fix_pathinfo导致shell.jpg被解析为PHP。当URL是/uploads/shell.jpg/x.php时,PHP-FPM在cgi.fix_pathinfo=1的情况下会向后查找直到找到shell.jpg当成PHP执行。解决方法是把php.ini里的cgi.fix_pathinfo改为0,或者在Nginx的fastcgi_pass之前加一条try_files验证文件真实存在。

坑3:宝塔面板的Nginx配置被自动重写覆盖。宝塔在某些操作(如修改伪静态、新增SSL)会重新生成Nginx配置,把我们手动加的禁用规则覆盖掉。解决方法是把规则写在include文件里(如/www/server/panel/vhost/nginx/include/security.conf)然后在主配置里include这个文件,这种方式不会被宝塔的自动重写覆盖。

坑4:Cloudflare的Always Use HTTPS规则导致403被改写为301。当我们的禁用规则返回403时,Cloudflare的某些Page Rule会把403改写为301重定向到HTTPS版本,攻击者跟着重定向访问HTTPS版本可能因为另一台后端服务器没配置规则而成功。解决方法是确认所有后端节点都配置了禁用规则,或在Cloudflare Workers脚本里直接拦截带特定扩展名的请求。

坑5:DedeCMS后台的远程文件下载功能绕过禁用规则。DedeCMS后台有个采集功能可以远程下载文件保存到uploads目录,绕过常规上传限制。即使uploads目录禁用了PHP执行,攻击者也可以下载到data或templets目录(如果这两个目录没有被禁用)。解决方法是把data和templets也加入禁用清单,或者直接在DedeCMS后台禁用采集模块。

配套的服务器层加固建议

禁用目录PHP执行只是纵深防御的第一层,真正的安全需要多层叠加:

文件权限管理:目录权限755(rwxr-xr-x),文件权限644(rw-r--r--),所有者是root或专门的部署用户而不是www-data。这样即使webshell成功上传也只有读权限不能写。

SELinux或AppArmor:CentOS、RHEL系统默认启用SELinux,把httpd_sys_content_t类型的文件设为只读,httpd_sys_rw_content_t设为可写但不可执行,这种类型层面的强制访问控制比纯文件权限更难绕过。

open_basedir限制:在php.ini或Nginx配置里给每个站点单独配置open_basedir,把PHP的文件读写权限限制在站点目录之内,即使有任意文件读取漏洞也不会泄露其他站点或系统文件。

disable_functions:在php.ini里禁用exec、shell_exec、system、passthru、proc_open、popen等危险函数,让常见的命令执行类webshell失效。

WAF:阿里云WAF、安全狗、ModSecurity都能在请求层拦截已知的webshell行为模式。WAF与我们这套服务器层硬化是互补关系而不是替代关系,建议同时部署。

规则验证的标准流程

我的标准验证流程不依赖浏览器,全用命令行:

第一步在受保护目录里放一个测试文件,内容是简单的回声语句(echo PWNED之类)。第二步用curl访问该文件,预期返回HTTP 403 Forbidden。第三步如果返回200且看到回声内容,规则没生效,回去检查location顺序、AllowOverride设置、文件是否上传到位、Web Server是否真正reload了。第四步测试完立刻删掉测试文件。

对于双扩展名兜底规则的验证,把测试文件命名为test.php.jpg,访问URL是/uploads/test.php.jpg,预期同样返回403。如果返回200且PHP代码被执行,说明双扩展名规则没生效,可能是Apache的mod_mime配置或Nginx的try_files顺序有问题。

对于黑客模拟测试,可以用curl的--user-agent参数伪造爬虫UA、用-H头伪造Referer,全方位测试规则的覆盖度。我自己写了一个小bash脚本批量测试常见绕过手段,每次部署完跑一遍5秒出结果,比手动测试可靠得多。

常见问题解答

禁用之后我自己的PHP入口比如uploads/install.php也跑不了怎么办?

有两种处理方式。第一种是加白名单,在禁用规则后面加一条精确匹配的location(Nginx)或Files块(Apache),把那个特定文件单独放行重新指向PHP-FPM。第二种更推荐的做法是部署完就把这种安装脚本删掉,这本来就是OWASP推荐的硬化项。安装类脚本作为长期暴露在互联网上的PHP入口本身就是高风险。如果必须保留也要用HTTP Basic Auth加IP白名单双重保护。

图片可以正常访问吗会不会把jpg或png也拦了?

不会。所有规则都精确匹配以.php、.phtml、.pht、.phar、.php3、.php5、.php7结尾的文件后缀,jpg、png、gif、webp、css、js、woff、ttf等扩展名完全不受影响。规则的关键正则部分用了\.(php|phtml|...)$这种带美元符号锚点的写法,确保只匹配文件结尾的扩展名而不会误伤路径中间包含php字样的目录或文件名(如/uploads/myphpsong.jpg是合法的)。

攻击者改写.htaccess怎么办?

这是.htaccess方案的根本弱点。生产环境一定用vhost或Nginx server块里的硬规则,并且把web目录的.htaccess设为root所有、www用户只读(chown root加chmod 644)。再配合AllowOverride None干脆禁用htaccess解析,让攻击者即使改了.htaccess文件也不会被Apache读取。这种纵深防御组合下,攻击者必须先拿到root权限才能突破,难度跃升一个量级。

禁用规则会影响网站性能吗?

影响极小。Nginx的正则location匹配是O(n)线性扫描,一条额外的禁用location只增加几微秒。Apache的vhost规则解析是启动时一次性完成,运行时几乎零开销。.htaccess方案因为每次请求都要重新解析有约5%到10%性能开销,是这套方案中性能最差的,但对一般中小站点(每秒请求数小于100)感知不到。

禁用规则与CDN缓存如何协同?

CDN(如Cloudflare、阿里云CDN)的缓存层在源站之前,如果攻击者请求被CDN缓存命中就不会走到源站,源站的禁用规则不参与判断。但CDN默认不缓存PHP扩展名的请求(HTTP方法是POST或URL含php扩展名),所以正常情况下PHP请求都会回源到源站,禁用规则有效。建议在CDN层也加一层文件类型禁止规则作为前置防线,纵深防御。

WordPress站点用这套规则需要注意什么?

WordPress的wp-content/uploads目录是默认上传目录必须禁用PHP执行。但要注意WordPress的wp-cron.php、wp-load.php、xmlrpc.php这些根目录PHP文件不能误伤。规则的覆盖范围只针对uploads子目录,根目录PHP文件不受影响。另外WordPress的某些插件(如WP Rocket缓存、ManageWP远程管理)会在uploads目录下创建PHP文件,需要根据插件文档加白名单或更换插件。

这套规则在Docker容器里怎么部署?

容器化部署的最佳实践是把禁用规则写在Nginx或Apache的镜像里(Dockerfile里COPY config文件),构建镜像时就把规则固化。运行时Web Server加载固化配置,攻击者即使突破容器也无法持久化修改规则(容器重启后规则恢复)。如果用docker-compose或Kubernetes,把配置文件挂载为只读Volume(read-only flag)防止运行时被改写。

规则部署完之后多久会被新的绕过手段突破?

从我8年的实战经验看,这套规则覆盖了PHP扩展名解析的全部已知绕过手段,至今没有遇到过被绕过的案例。但安全是动态的,每年至少做一次完整审计:检查PHP官方有没有新增扩展名处理(如phar在PHP 7才被广泛认知)、Web Server有没有版本更新引入新行为、CMS有没有新增上传入口(如某些插件用了非标准目录)。审计周期建议是新CMS版本发布或PHP大版本升级后立刻做一次。

非标准目录上传的攻击场景怎么办?

典型场景是攻击者利用应用层漏洞把文件写到tmp、log、session_save_path等非标准目录。防御方法是把整套禁用规则反过来实现:默认所有目录都不能跑PHP,只显式白名单需要执行PHP的目录(如根目录、admin/、api/)。这种白名单模式的安全性远高于黑名单,但配置工作量大、对CMS架构理解要求高,建议在架构清晰的项目里使用。

分享到
标签
版权声明

本文标题:《禁用目录PHP执行权限:5种服务器加固方案》

本文链接:https://zhangwenbao.com/method-of-disable-directory-permissions-for-php-directory-execution.html

版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交