force_download()不能直接用于敏感文件下载,因其仅封装readfile()和http头设置,不校验用户权限、不验证路径合法性、不阻止目录遍历,易导致任意文件读取;必须配合会话校验、id查库、白名单路径检查、扩展名过滤等完整访问控制链路。

直接用 force_download() 并不安全,它只控制HTTP头,不校验用户权限、不验证文件路径合法性、也不阻止目录遍历——上传后存到 ./uploads/ 的私有PDF,可能被拼出 /uploads/../logs/app.log 直接下载。
为什么 force_download() 不能直接用在敏感文件上
这个函数本质是 PHP 的 readfile() + 头设置封装,不经过任何访问控制逻辑。它默认信任传入的路径参数,而开发者常犯的错是把用户可控的 $_GET['file'] 拼进路径里,导致路径穿越或任意文件读取。
- 它不会检查当前用户是否已登录,也不会调用
$this->session->has_userdata('user_id') - 它不验证文件是否在白名单目录内(如仅允许
./downloads/下的文件) - 它不校验文件扩展名,攻击者可尝试下载
.env或config.php - 若传入相对路径且未做
realpath()归一化,../可逃逸限制
安全下载必须走控制器 + 权限校验 + 路径白名单
所有下载请求必须经由控制器方法处理,不能暴露真实文件路径给前端。核心是“代理模式”:URL 是 /download/123,控制器查数据库拿到该 ID 对应的真实路径和权限规则,再决定是否放行。
- 先校验会话:
if (!$this->session->has_userdata('user_id')) { show_403(); } - 用 ID 查记录:
$file = $this->db->where('id', $id)->get('downloads')->row();,避免用户传路径 - 白名单路径检查:
if (strpos($file->real_path, FCPATH . 'secure_downloads/') !== 0) { show_403(); } - 扩展名二次过滤:
$ext = pathinfo($file->real_path, PATHINFO_EXTENSION); if (!in_array($ext, ['pdf', 'xlsx', 'zip'])) { show_403(); } - 最后才调用:
force_download($file->real_path, $file->display_name);
force_download() 的实际使用陷阱
即使做了权限校验,调用 force_download() 时仍有几个硬性要求必须满足,否则会出错或降级为浏览器打开而非下载。
- 文件必须存在且 PHP 进程有读权限,否则报
Unable to open file - 路径必须是服务器上的绝对路径(推荐用
FCPATH或WRITEPATH拼接),相对路径容易失败 - 如果文件大于 2MB,需确保
output_buffering关闭或手动ob_end_clean(),否则可能截断 - 不要在调用前输出任何内容(包括空格、BOM、
echo),否则 header 已发送,触发Cannot modify header information - 下载大文件时建议加
ini_set('memory_limit', '256M');,避免Allowed memory size exhausted
最易被忽略的是路径归一化和扩展名校验这两步——很多团队只做了登录检查,却忘了攻击者可以注册账号后上传一个名为 shell.php.jpg 的文件,再通过下载接口绕过 MIME 检查执行代码。真正的安全下载,从来不是函数调用对不对,而是整个链路有没有堵死所有旁路。











