下载前必须校验登录态,使用session而非url参数;文件路径需白名单映射,禁用用户输入拼接;响应头须禁用缓存、设准确content-type与content-disposition;大文件应流式分块输出。

下载前必须校验登录态,不能只靠 URL 参数
PHP 8.3 下载接口若不强制验证用户身份,攻击者只需猜中文件路径(如 /download.php?file=../config.php)就能直接读取敏感文件。Session 是最稳妥的校验方式 —— 它把凭证存在服务端,客户端只持有一个不可伪造的 PHPSESSID Cookie。
常见错误是仅检查 $_GET['token'] 或写死一个密钥,这类方案极易被爆破或泄露。正确做法是在下载脚本开头就调用 session_start(),再判断 $_SESSION['user_id'] 是否存在且有效。
- 必须在任何输出(包括空格、BOM)之前调用
session_start() - 建议设置
session_set_cookie_params(['httponly' => true, 'secure' => true])防 XSS 和明文传输 - 不要用
isset($_SESSION)判断,而应检查具体键(如isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true) - 若使用 Redis 存储 session,确认
session.save_handler = redis已启用且连接正常
文件路径不能拼接用户输入,要用白名单映射
即使用户已登录,也不能让 $_GET['file'] 直接参与 file_get_contents() 或 readfile() 调用。路径遍历(../../etc/passwd)、空字节截断(shell.php%00.jpg)仍可绕过简单过滤。
安全做法是:只接受预定义的标识符(如 report_q3、invoice_2026),再通过关联数组映射到真实路径。这样既杜绝路径注入,也避免暴露服务器目录结构。
- 示例白名单映射:
$allowed_files = ['q3_report' => '/var/data/reports/q3_final.pdf', 'user_guide' => '/var/data/docs/guide_v2.1.pdf']; - 拒绝所有未在白名单中的
$_GET['id']值,直接返回 404 或 403 - 真实路径必须绝对、不带用户可控部分,且不在 Web 根目录下(如
/var/data/而非/var/www/html/files/) - 映射后的文件需再次用
is_readable()检查,防止因权限变更导致意外可读
下载响应头要禁用缓存并设对 Content-Type
浏览器或中间代理缓存未授权的下载响应,可能造成越权访问残留。同时,错误的 Content-Type 可能触发 IE 或旧版 Edge 的 MIME 类型嗅探,导致 PHP 文件被解析执行而非下载。
PHP 8.3 中应显式关闭缓存,并为每个文件类型设置精确的 MIME 类型(不是全用 application/octet-stream)。尤其对 .php、.htaccess 等高危扩展名,必须确保它们永远不会被当作脚本执行。
- 关键响应头:
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');、header('Pragma: no-cache');、header('Expires: 0'); - 用
finfo_open(FILEINFO_MIME_TYPE)动态获取真实 MIME 类型,比硬编码更可靠(例如 PDF 必须是application/pdf,不是text/plain) - 务必设置
Content-Disposition: attachment; filename="safe_name.pdf",且filename值需过滤掉双引号、换行等危险字符 - 避免使用
Content-Transfer-Encoding(HTTP/1.1 已废弃),它反而可能干扰现代浏览器解析
大文件下载要流式输出,别用 readfile() 硬扛
当文件超过几十 MB,readfile() 会一次性加载进内存,容易触发 PHP 内存限制(memory_limit)或超时(max_execution_time),还可能被 CDN 或反向代理截断。
正确方式是分块读取、逐块输出,配合 ob_flush() 和 flush() 控制缓冲,让客户端持续接收数据。这对支持断点续传(Accept-Ranges)和监控下载进度也更友好。
- 用
fopen($path, 'rb')打开只读流,再循环fread($fp, 8192)输出 - 每次输出后调用
ob_flush()和flush(),但注意某些 SAPI(如 PHP-FPM)可能忽略flush() - 若需断点续传,必须解析
$_SERVER['HTTP_RANGE'],返回206 Partial Content及对应Content-Range头 - 大文件场景建议加日志记录:
error_log(sprintf('[DOWNLOAD] %s %s %s', $_SERVER['REMOTE_ADDR'], $real_path, date('c')));
dev_config.php,上线后忘了从白名单里删掉,而该文件又恰好被放到了可读路径下 —— 这类配置漂移比代码漏洞更难审计。php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











