文件包含漏洞的本质是程序在加载文件时过度信任了用户可控的输入参数。攻击者并不需要直接上传一个完整的木马文件,只要应用存在文件包含点,并且相关配置或代码逻辑不够严谨,就能把服务器上已有的文件、日志、图片甚至是伪协议内容当作代码执行。这种攻击方式之所以危险,是因为它绕过了传统文件上传防护,直接利用应用自身的文件加载机制完成恶意代码植入。
要真正杜绝文件包含漏洞,必须从代码层、配置层和运行环境层同时下手。单纯修补某一个点往往无法阻断完整攻击链。比如只过滤../却不限制协议,攻击者仍然可以利用php://input或data://执行任意代码;只关闭远程包含却不处理本地文件路径,攻击者依然可以通过包含日志文件或会话文件拿到WebShell。下面从几个关键维度给出具体可落地的防护方案。
一、优先采用白名单映射机制最有效的防护不是过滤危险字符,而是从根本上取消用户输入对文件路径的控制权。将用户传入的参数作为映射键,通过固定的数组或配置文件查找真实文件路径。例如用户传入的参数值只允许是page、news、about等固定标识,后端根据标识返回预先定义好的文件。
$allowed = [
'page' => '/var/www/includes/page.php',
'news' => '/var/www/includes/news.php',
'about' => '/var/www/includes/about.php'
];
$key = $_GET['file'] ?? '';
if (isset($allowed[$key])) {
include($allowed[$key]);
} else {
exit('Invalid file parameter');
}
这种做法的核心优势在于,无论攻击者如何构造参数,最终被包含的文件只能来自开发者预先设定的集合。即便参数中带有../../、绝对路径或伪协议,也无法匹配到白名单键名。这是目前公认最稳妥的防文件包含方案,尤其适合功能固定、包含文件数量有限的中小型应用。
二、严格过滤并限制路径格式如果业务场景确实需要动态拼接文件路径,例如根据模块名和文件名加载不同功能文件,则必须对输入参数做严格格式校验。建议使用正则表达式限定参数只能包含字母、数字、下划线和短横线,长度不超过预设值,并且不允许出现点号、斜杠、反斜杠等路径分隔符。
$file = $_GET['file'] ?? '';
if (!preg_match('/^[a-zA-Z0-9_\-]{1,50}$/', $file)) {
exit('Invalid file name');
}
$path = '/var/www/includes/' . $file . '.php';
if (file_exists($path)) {
include($path);
} else {
exit('File not found');
}
这里的关键是校验发生在任何文件系统操作之前,并且校验规则足够严格。只允许字母数字下划线短横线,直接排除了路径穿越所需的点号和斜杠。同时通过file_exists判断文件是否真实存在,可以避免包含不存在的文件时产生额外报错信息。需要注意,file_exists本身并不能防止包含漏洞,它只是辅助判断,真正的安全屏障仍然是前面的格式校验。
三、禁用远程文件包含与危险伪协议PHP中远程文件包含的开关allow_url_include默认关闭,但很多历史项目或错误配置会将其打开。一旦开启,攻击者可以直接包含远程服务器上的恶意脚本。即使远程包含关闭,php://input、php://filter、data://等伪协议依然可能被利用。php://input可以读取POST原始数据并当作PHP代码执行,data://可以直接内联代码,php://filter则可用于读取文件内容。
在php.ini中应明确设置:
allow_url_include = Off allow_url_fopen = Off
allow_url_fopen关闭后,不仅远程包含被禁止,很多基于URL的文件操作函数也会受限。如果业务确实需要读取远程资源,建议改用curl扩展单独实现,而不是依赖文件包含机制。对于伪协议,可以在代码中检测参数是否包含://、php://、data://等字符串,一旦发现立即拒绝。更彻底的做法是在应用入口处统一过滤所有用户输入中的协议标识。
四、限制包含文件的目录范围即使参数经过过滤,仍然建议将可包含文件集中存放在独立目录中,并在代码中强制拼接固定前缀路径。这样即使攻击者构造出某种绕过过滤的路径,最终也只能在限定目录内寻找文件。例如将所有可包含文件放在/var/www/app/includes/目录下,代码中固定拼接该前缀,并再次使用realpath函数获取规范化后的绝对路径,然后检查该路径是否仍位于允许目录内。
$baseDir = '/var/www/app/includes/';
$file = $_GET['file'] ?? '';
if (!preg_match('/^[a-zA-Z0-9_\-]+$/', $file)) {
exit('Invalid file name');
}
$fullPath = realpath($baseDir . $file . '.php');
if ($fullPath === false || strpos($fullPath, $baseDir) !== 0) {
exit('Access denied');
}
include($fullPath);
realpath会解析掉路径中的../和./,返回规范化的绝对路径。通过strpos检查该路径是否以允许目录开头,可以彻底杜绝路径穿越。即便参数中混入了编码后的斜杠或反斜杠,realpath也能在文件系统层面还原真实路径,从而被检查逻辑拦截。
五、日志文件与上传文件的间接包含防护很多文件包含漏洞的利用并不直接包含参数指定的文件,而是通过包含服务器日志、会话文件、临时上传文件等方式间接执行恶意代码。攻击者先向应用发送带有PHP代码的请求,使代码被记录到访问日志或错误日志中,再利用文件包含点包含该日志文件,从而执行其中的PHP代码。这类攻击对日志目录的可读性和包含点的路径可控性有较高要求。
防护措施包括:将日志目录设置在Web根目录之外,限制Web进程对日志目录的读取权限;定期清理或轮转日志文件;在日志记录时对特殊字符进行转义或过滤,避免原始请求内容原样写入日志。对于会话文件,应将会话存储路径设置在非Web可访问目录,并设置严格的目录权限。对于上传目录,应禁止脚本执行权限,例如在Apache中通过.htaccess设置php_flag engine off,在Nginx中通过location规则禁止对上传目录的PHP解析。
六、错误信息控制与安全监控文件包含失败时,PHP默认会输出包含路径的错误信息,这些信息可能暴露服务器目录结构、文件命名规则等敏感内容,为攻击者提供进一步利用的线索。生产环境应关闭详细错误显示,将错误写入日志文件而非直接输出到页面。
display_errors = Off log_errors = On error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
同时建议在应用层增加安全监控,对包含操作进行日志记录。每次包含文件时记录请求来源IP、请求参数、包含的最终路径和时间戳。当检测到同一来源短时间内多次尝试包含不存在的文件或包含路径包含异常字符时,触发告警或临时封禁。这种监控不能直接阻止漏洞利用,但能帮助快速发现攻击行为并追溯攻击源。
七、框架与CMS的二次开发注意事项使用ThinkPHP、Laravel、WordPress等框架或CMS时,很多人认为框架本身已经处理了文件包含问题,从而忽视二次开发中的风险。实际上,框架核心代码通常对路由和控制器加载做了严格限制,但开发者在插件、主题或自定义模块中直接使用include、require并拼接用户输入的情况并不少见。这类代码往往绕过框架的安全机制,成为攻击入口。
在二次开发中应遵循框架的模块加载规范,使用框架提供的视图渲染和模块调用方法,而不是直接操作文件路径。如果必须包含动态文件,同样要应用白名单或严格过滤策略。定期检查自定义代码中是否存在include($var)、require($_GET[...])这类危险模式,使用静态代码扫描工具辅助发现潜在问题。
八、纵深防御与最小权限原则文件包含漏洞的最终危害程度与Web进程的运行权限密切相关。如果PHP进程以root或高权限用户运行,一旦被包含恶意代码,攻击者可以读取任意系统文件、修改配置、植入持久化后门。应遵循最小权限原则,为Web进程创建独立用户,仅授予应用目录的读写权限,禁止访问系统敏感目录。
同时利用操作系统层面的防护机制,例如SELinux或AppArmor,限制Web进程的文件访问范围。即使应用层出现漏洞,操作系统强制访问控制也能阻止对关键系统文件的读取和执行。这种纵深防御思路虽然不能替代代码修复,但能显著降低漏洞被利用后的影响范围。
文件包含漏洞防护的核心在于消除用户输入对文件路径的直接控制。白名单映射是首选方案,严格格式校验是必要补充,配置加固和权限限制是兜底手段。只有将代码层、配置层和系统层防护结合起来,才能真正杜绝恶意攻击代码通过文件包含漏洞植入服务器。
