PHP文件分享需用readfile()配合正确响应头,禁用路径拼接、强制白名单校验、处理中文文件名编码,并设Content-Type与Content-Disposition,最后exit终止脚本。

PHP 实现简单文件分享,核心是控制文件读取 + 安全响应,不是直接暴露文件路径。 直接用 readfile() 或 fpassthru() 配合 header() 设置 MIME 类型即可,但绕过权限校验、忽略文件名编码、不设缓存头,会导致下载乱码、404、或任意文件读取漏洞。
用 readfile() 输出文件并正确设置响应头
这是最常用也最容易出错的方式。关键不在“能不能读”,而在“怎么告诉浏览器这是个可下载的文件”。
- 必须在输出前调用
header('Content-Type: ...'),类型要尽量准确(如application/pdf),不确定时可用application/octet-stream - 必须设置
Content-Disposition: attachment; filename="xxx",否则浏览器可能尝试内嵌打开(比如 PDF、图片) - 中文文件名需用
filename*=UTF-8''...编码,否则 Chrome/Firefox 下显示为乱码或下划线 - 务必调用
exit;或die();阻止后续脚本输出(哪怕只多一个空格也会破坏二进制流)
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="report.pdf"');
header('Content-Disposition: attachment; filename*=UTF-8\'\'%E6%8A%A5%E5%91%8A.pdf');
readfile('/var/www/files/report.pdf');
exit;
为什么不能直接 echo file_get_contents($path)
看似等价,但实际风险高、效率低:
-
file_get_contents()会把整个文件加载进内存,大文件(>100MB)容易触发memory_limit错误 - 如果文件路径来自用户输入(如
$_GET['f']),且没做白名单校验,攻击者可构造?f=../../etc/passwd读取系统敏感文件 - PHP 输出缓冲区若已开启,
echo可能被截断或混入额外字符,导致文件损坏 -
readfile()是底层 C 实现,边读边输出,内存占用恒定,更可靠
如何安全地解析用户传入的文件标识
永远不要拼接用户输入到文件路径里。推荐用映射表或哈希 ID 替代真实文件名:
立即学习“PHP免费学习笔记(深入)”;
- 数据库存
share_id→real_path映射,查询后校验路径是否在允许目录内(如realpath($path) === 0 === strpos(realpath($path), '/var/www/shared/')) - 或生成短哈希(如
sha256($real_path . $secret_key))作为 URL 参数,服务端反查对应文件 - 禁止使用
..、/、空字节等路径遍历字符,可用basename($user_input)截取最后一段,再拼到固定目录下 - 对文件扩展名做强制白名单(如只允许
['pdf', 'txt', 'zip', 'png']),避免执行脚本(.php、.phar)
忽略 Content-Transfer-Encoding 和缓存头的后果
这两个常被遗漏,但直接影响用户体验和安全性:
- 不设
Cache-Control: no-cache, private,CDN 或代理可能缓存响应,导致旧文件被反复分发 - 不设
Expires: 0,某些老旧客户端可能复用过期响应 -
Content-Transfer-Encoding已过时(HTTP 不需要 Base64 编码),设了反而干扰解析,PHP 中完全不用管 - 大文件建议加
header('Accept-Ranges: bytes')和处理Range请求,但简单分享可省略
真正麻烦的不是代码几行,而是路径校验逻辑是否覆盖所有边界(比如符号链接绕过、大小写差异、Windows 驱动器字母)、文件名编码在不同浏览器下的兼容性、以及下载中断后服务端有没有清理临时状态。这些细节不测试到位,上线后第一份分享文档就打不开。



















