PHP 伪协议避坑指南,新手老手都得看的文件包含漏洞解析
上个月帮一个朋友排查线上问题,遇到个挺有意思的 case。他们的系统有个功能:根据用户传入的参数动态加载模板文件。代码长这样:
$template = $_GET['tpl'];
include('/var/www/templates/' . $template . '.php');
看着挺正常对吧?但问题是,有人传了个 ../../../../etc/passwd 进来,直接把服务器密码文件读走了。更骚的是,还有人传了 php://filter/convert.base64-encode/resource=../config/database.php,数据库配置源码全给扒光。
朋友一脸懵逼:php:// 这是个啥?怎么还能这么玩?
其实,这是 PHP 伪协议在作祟。哪怕到了 2026 年,PHP 版本已经迭代多次,但这套机制依然坚挺,且在新版本中细节更加丰富。很多开发者只把它当成“读取文件”的另类写法,却忽略了它背后巨大的能量与风险。今天我们就从实战角度,把 PHP 伪协议掰开揉碎了讲讲,既包括它的核心用法,也涵盖那些让你防不胜防的安全坑点。
伪协议到底是什么?
先别被“伪协议”这三个字唬住。说白了,它就是 PHP 定义的一套“特殊路径标识”,让你能用 fopen()、file_get_contents()、include() 这些原本只操作本地文件的函数,去访问不只是普通文件的东西。
比如你想读一个文件:
// 普通读文件
$content = file_get_contents('/etc/passwd');
// 用伪协议读,效果一样
$content = file_get_contents('file:///etc/passwd');
file:// 是最基础的伪协议,默认甚至可以省略。但 PHP 内置了十几种伪协议,有的能读压缩包里的文件,有的能直接处理 POST 数据,有的能对文件内容做实时过滤。理解它们,不仅能帮你写出更灵活的代码,更能让你在审查代码时一眼看出潜在漏洞。
核心伪协议详解与实战
file://:最老实本分的那个
file:// 没啥花活,就是访问本地文件系统。
// Linux
file_get_contents('file:///etc/passwd');
// Windows
file_get_contents('file://C:/Windows/win.ini');
这个协议默认开启,而且不受 allow_url_fopen 和 allow_url_include 的影响——就算这两项配置全关了,file:// 照样能用。这也是为什么很多以为关了远程包含就万事大吉的开发者的盲区:攻击者依然可以利用它读取本地敏感文件。
php://filter:真正的“瑞士军刀”
这是最常用也最危险的协议,没有之一。它允许你在读取文件的同时,对内容应用一个或多个过滤器。
// 把文件内容 base64 编码后输出
file_get_contents('php://filter/read=convert.base64-encode/resource=secret.php');
// 多个过滤器链式调用:先转 base64,再转 rot13
file_get_contents('php://filter/read=convert.base64-encode|string.rot13/resource=secret.php');
为什么需要这个?最常见的场景是:通过文件包含漏洞读取 PHP 源码时,直接 include PHP 文件会被解析执行,你看不到源码。但加上 base64 过滤器,输出的就是编码后的内容,不会执行,解码就能看到源码。
在 2026 年的今天,php://filter 依然是最实用的调试工具之一。有时候线上环境不方便开 Xdebug,但你又想看某个配置文件的原始内容,就可以临时写个脚本用 filter 读一下。
支持的过滤器类型非常多:
- 字符串过滤器:
string.rot13、string.toupper、string.tolower - 转换过滤器:
convert.base64-encode/decode、convert.quoted-printable-encode/decode - 压缩过滤器:
zlib.deflate、bzip2.compress - 字符集转换:
convert.iconv.GBK/UTF-8(处理老系统乱码神器)
php://input:接收原始 POST 数据
这个协议用来读取 HTTP 请求的原始 body。
// 不管 Content-Type 是啥,都能拿到原始数据
$rawData = file_get_contents('php://input');
$jsonData = json_decode($rawData, true);
以前很多新手踩坑:前端传 JSON,后端用 $_POST 拿,结果空的。因为 $_POST 只解析 application/x-www-form-urlencoded 或 multipart/form-data。要拿 JSON,就得从 php://input 读。
有个细节需要注意:当 enctype="multipart/form-data" 时,php://input 是无效的,读不到东西。在前后端分离已成标配的今天,这个协议的使用频率比十年前高太多了。
data://:把字符串当文件用
这个协议可以把一个字符串当成文件内容来处理,常用于绕过文件上传限制或在 CTF 中快速执行代码。
// 直接把字符串当文件包含
include('data://text/plain,<?php phpinfo(); ?>');
// base64 版本(绕过特殊字符过滤)
include('data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=');
在实际开发中也有正经用途——比如测试的时候,不想真的写文件,直接用 data 协议构造测试数据传入函数,验证逻辑是否正确。
zip:// 和 phar://:压缩包里的秘密
这两个协议用来读取压缩包内的文件,无需解压。
// zip 协议:注意#要编码成%23
$content = file_get_contents('zip:///path/to/archive.zip%23file.txt');
// phar 协议:路径格式不同
$content = file_get_contents('phar:///path/to/archive.phar/file.txt');
两者区别在于分隔符:zip:// 用 #(URL 编码为 %23),phar:// 用 /。
更危险的是,PHP 解析 Phar 文件时会触发反序列化操作。所以如果程序有文件包含漏洞,结合 Phar 协议和反序列化漏洞,能打出组合拳,直接导致远程代码执行。
深度进阶:过滤器链与自定义流
链式过滤器的艺术
php://filter 最骚的地方是支持多个过滤器组合,像流水线一样处理数据。
// 先 base64 解码,再 rot13,再转小写
$content = file_get_contents(
'php://filter/read=convert.base64-decode|string.rot13|string.tolower/resource=encrypted.txt'
);
这在处理特定格式的数据时非常有用。比如有些老系统会把配置文件用简单算法混淆,你可以用 filter 链直接在读取时还原,省去了写额外解密脚本的麻烦。
自定义协议:用类实现你的流
如果你觉得内置协议不够用,PHP 还允许你自己注册协议——这就是 stream_wrapper_register()。
比如你想实现一个 var:// 协议,读写全局变量:
class VariableStream {
private $position = 0;
private $varname;
public function stream_open($path, $mode, $options, &$opened_path) {
$url = parse_url($path);
$this->varname = $url["host"];
$this->position = 0;
return true;
}
public function stream_read($count) {
$ret = substr($GLOBALS[$this->varname], $this->position, $count);
$this->position += strlen($ret);
return $ret;
}
// 还需实现 stream_write, stream_close 等方法...
}
// 注册协议
stream_wrapper_register("var", "VariableStream");
// 使用
$GLOBALS['foo'] = "Hello World";
echo file_get_contents("var://foo"); // 输出了 Hello World
这个例子虽然像个玩具,但思路打开了:你可以实现数据库流、Redis 流、甚至是 API 流——用统一的文件操作接口访问任何数据源,让代码架构更加优雅。
安全:伪协议的 AB 面
伪协议是双刃剑,用好了是神器,用坏了是灾难。在文件包含漏洞中,它往往是攻击者的放大器。
典型攻击场景
假设你有这样一段漏洞代码:
include($_GET['file']);
攻击者可以利用伪协议做很多坏事:
- 读源码:
?file=php://filter/convert.base64-encode/resource=index.php - 执行任意代码:
?file=data://text/plain,<?php system('id'); ?> - 读系统文件:
?file=file:///etc/passwd
哪怕你限制了后缀名,攻击者依然可以通过 php://filter 绕过,因为最终包含的不是 .php 文件,而是一个经过过滤处理的流。
防护措施:三道防线
第一道防线:配置加固
在 php.ini 中,必须严格管控以下选项:
; 必须关!控制 include/require 能否访问远程文件或 php://input
allow_url_include = Off
; 可以开,但要配合其他措施,控制 file_get_contents 等函数
allow_url_fopen = On
生产环境中,allow_url_include 必须设为 Off,这是底线。
第二道防线:代码层面的白名单
永远不要信任用户输入的路径。使用白名单校验是最稳妥的办法:
$allowed_pages = ['home', 'about', 'contact'];
$page = $_GET['page'] ?? 'home';
if (in_array($page, $allowed_pages)) {
include('pages/' . $page . '.php');
} else {
http_response_code(404);
echo 'Page not found';
}
如果必须动态加载,务必使用绝对路径并校验前缀:
$base = '/var/www/templates/';
$input = $_GET['tpl'];
// 防止路径穿越
$file = realpath($base . $input);
if ($file && strpos($file, $base) === 0 && file_exists($file)) {
include($file);
} else {
die('Invalid file path');
}
第三道防线:输入过滤与清洗
除了 $_GET、$_POST,很多地方也藏着危险:$_SERVER['HTTP_REFERER']、$_SERVER['QUERY_STRING']、$_FILES['file']['name'] 等。这些字段用之前都要过一遍 filter_var() 或正则表达式,剔除非法字符。
特别是当你要使用 php://input 读取 JSON 时,也要做校验:
$raw = file_get_contents('php://input');
$data = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
die('Invalid JSON');
}
// 再对 $data 做字段校验...
2026 年新视野:伪协议的“正经”用途
说了这么多漏洞,但伪协议本身是中性的。在 2026 年的现代 PHP 开发中,它们在不少正经场景里发挥着重要作用。
流式处理大文件
用 php://temp 或 php://memory 处理临时数据,可以避免内存爆炸:
// 把大文件内容写到临时流
$fp = fopen('php://temp', 'r+');
fwrite($fp, $hugeData);
rewind($fp);
// 处理流中的数据...
while (!feof($fp)) {
$chunk = fread($fp, 8192);
// process $chunk
}
fclose($fp);
php://temp 会在数据超过 2MB 时自动转成临时文件,比纯内存的 php://memory 更安全,适合处理不确定大小的数据流。
API 响应的动态压缩
利用 compress.zlib:// 可以直接读取压缩过的远程资源,无需落地磁盘:
// 读取压缩包里的文件,不解压到磁盘
$content = file_get_contents('compress.zlib://http://example.com/data.gz');
这在微服务架构中非常有用,可以减少网络传输体积,提升系统整体性能。
伪协议就像一把精密的手术刀,在资深开发者手中能切除病灶、优化架构,但在缺乏安全意识的新手手里,却可能变成自残的利器。理解它们的原理,掌握防御的技巧,才能在 2026 年及以后的 PHP 开发之路上走得更稳、更远。
更多推荐

所有评论(0)