上传目录必须移出public目录,如设为runtime/uploads,并通过web服务器配置禁止脚本执行;上传后须用finfo_file校验真实mime类型及扩展名白名单;下载时需php控制readfile()输出并设置安全响应头。

上传目录不能放在 public 下直出
ThinkPHP 默认把上传文件存到 public/uploads 这类路径,浏览器能直接通过 URL 访问,等于把文件暴露在 Web 根目录下。一旦路径被猜中(比如 /uploads/2024/config.php),敏感文件就可能被下载或执行。
正确做法是把上传目录移到 public 外面,比如 runtime/uploads 或 storage/uploads。这样 Nginx/Apache 默认无法通过 HTTP 直接访问,必须走 PHP 控制逻辑才能读取。
- 修改上传配置:
'root_path' => runtime_path('uploads/') - 确保
runtime目录可写,且 Web 服务器用户(如 www-data)有读写权限 - 别在
public下建软链接指向外部上传目录——多数 Web 服务器会跟随符号链接,依然不安全
moveFile() 后要校验文件 MIME 和扩展名
仅靠前端或 file.type 判断类型不可信,攻击者可伪造请求头或改后缀绕过。ThinkPHP 的 moveFile() 不做深度检测,默认只看扩展名。
必须手动加一层校验:先用 finfo_file() 读取真实 MIME,再比对白名单;同时剥离扩展名中的点、空格、双扩展(如 shell.php.jpg)。
- 用
finfo_open(FILEINFO_MIME_TYPE)获取真实类型,拒绝application/x-php、text/x-php等可执行类型 - 用
pathinfo($file->getInfo('name'), PATHINFO_EXTENSION)提取扩展名,转小写后匹配预设白名单(如['jpg', 'png', 'pdf']) - 禁止保存文件名含
.htaccess、.user.ini、.php等敏感字符串(即使扩展名合法)
需要提供下载时,务必用 readfile() + header() 控制输出
如果业务要求用户能下载上传的文件(比如合同、图片),绝不能把文件路径拼成公开 URL 返回。否则相当于又开了个遍历入口。
所有下载请求必须走控制器,由 PHP 读取文件内容并输出,同时设置严格响应头,防止浏览器误解析或缓存敏感内容。
- 用
is_file($realPath) && is_readable($realPath)先校验文件存在且可读 - 用
basename($filename)强制重命名响应文件名,避免原始文件名带路径遍历字符(如../../etc/passwd) - 设置
Content-Disposition: attachment; filename="safe_name.pdf"和X-Content-Type-Options: nosniff
Nginx/Apache 要禁用上传目录下的脚本执行
即使文件存到 runtime/uploads,如果 Web 服务器配置不当,仍可能因路径别名、fastcgi_pass 泄露等原因导致 PHP 文件被执行。
必须在 Web 服务器层强制限制:对上传目录下的所有请求,一律返回 403 或直接拒解译。
- Nginx 示例:
location ^~ /runtime/uploads/ { deny all; }(注意不是location ~ \.php$,那是治标不治本) - Apache 示例:在
runtime/uploads/.htaccess中写Deny from all,并确认AllowOverride All已启用 - 检查是否启用了
auto_prepend_file或全局include_path,它们可能让非 PHP 文件也被解析
真正难的是把「权限控制」拆成三段:存储路径隔离、文件内容校验、Web 层执行拦截。少一段,上传功能就可能变成后门入口。尤其是开发环境常忽略 Nginx 配置,上线才暴露问题。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











