force_download()不可直接用于生产环境,因其无权限校验、易受路径遍历攻击、不审计下载行为且大文件易oom;应限定路径白名单、校验用户权限,并优先采用手动流式输出控制响应头与分块读取。

CodeIgniter 本身不提供“文件下载服务”,它只提供 force_download() 这个辅助函数,用于触发浏览器下载本地已存在的文件 —— 它不做权限校验、不走路由拦截、不检查用户身份,只是简单地设置响应头并输出文件内容。直接暴露文件路径给前端或绕过业务逻辑调用,等于把服务器文件系统大门敞开。
为什么 force_download() 不能直接用在生产环境
这个函数本质是 PHP 的 readfile() 封装,调用时只要传入一个真实存在的文件路径,就会强制下载。问题在于:
- 它不验证当前用户是否有权访问该文件(比如某用户改 URL 中的
id=123变成id=124,就可能下载别人上传的合同) - 它不校验文件是否属于应用可控范围(比如传入
/etc/passwd或../config/database.php,若路径拼接没过滤,直接读取敏感系统文件) - 它不记录下载行为(审计、限频、统计都落空)
- 它不处理大文件流式传输(
readfile()会一次性加载进内存,超大文件易导致 OOM)
force_download() 正确调用前提:路径必须白名单化或动态生成
不要让用户决定下载什么文件。所有可下载的文件路径,必须由后端根据业务规则生成,且限定在指定目录内。推荐做法:
- 上传成功后,把文件信息(如原始名、哈希、归属用户 ID、存储路径相对值)存进数据库,不存绝对路径
- 下载接口接收业务 ID(如
download/5678),查库拿到对应文件的stored_name和user_id - 用
FCPATH . 'uploads/' . $stored_name拼接真实路径,再用is_file()+realpath()+strpos()校验是否仍在uploads/目录下 - 确认用户有权限后,才调用
force_download($full_path, $original_name)
示例关键校验逻辑:
$full_path = FCPATH . 'uploads/' . $row->stored_name;
if (!is_file($full_path)) {
show_404();
}
// 防路径遍历
if (strpos(realpath($full_path), realpath(FCPATH . 'uploads/')) !== 0) {
show_403();
}
替代方案:用控制器流式输出,更可控
当需要加权限、限速、日志、断点续传或处理超大文件时,force_download() 就不够用了。应手动控制响应头和文件流:
- 用
header('Content-Type: application/octet-stream')明确类型 - 用
header('Content-Disposition: attachment; filename="' . $safe_name . '"')设置下载名(注意转义双引号) - 用
header('Content-Length: ' . filesize($full_path))告知大小 - 对大文件,用
fopen()+fread()分块读取输出,避免内存溢出 - 每输出一段后调用
ob_flush()+flush()确保及时送达
这种写法虽然多几行,但能嵌入鉴权、埋点、异常捕获等逻辑,才是生产级下载的常态。
公共目录文件被直接访问?.htaccess 不是万能解药
很多人把文件放 public/ 或 assets/ 下,指望 .htaccess 拦住请求。但要注意:
-
.htaccess只对 Apache 生效,Nginx 用户得配location规则,且容易漏配 - 如果文件本就不该公开(如用户私有附件),就不该放在 Web 可达路径下,而应放在
application/uploads/这类 Web 根目录外的位置 - 哪怕加了
Deny From All,如果控制器里用readfile()读取,照样能绕过 —— 权限控制必须在 PHP 层做,不能依赖 Web 服务器配置
最稳妥的路径结构是:上传目录设为 FCPATH . 'private_uploads/'(完全不在 Web 路径中),所有下载全部走控制器逻辑,彻底切断直接访问可能。











