文件上传的安全边界
文件上传是 Web 应用里攻击面最大的功能之一:传上去的不是文件,而是可能被执行的代码。
第一道:类型校验
扩展名和 Content-Type 都由客户端控制,不可信。校验顺序:
- 扩展名白名单(不是黑名单——黑名单永远漏);
- 服务端探测真实类型:图片用
getimagesize()或文件头魔数; - 拒绝
phtml、php5、shtml这类变体。
$allow = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'zip', 'docx'];
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
if (!in_array($ext, $allow)) reject('不支持的类型');
第二道:存储隔离
上传目录必须不可执行,这是最重要的一条。即使校验被绕过,文件也无法变成代码:
# Apache:目录内禁止脚本处理
<FilesMatch "\.(php|phtml|php[0-9]|shtml)$">
RemoveHandler .php .phtml .php3 .php4 .php5 .php7 .php8 .phtml .shtml
RemoveType .php .phtml .shtml
</FilesMatch>
Options -ExecCGI -Indexes
Nginx 对应 location ~* ^/uploads/.*\.(php|phtml)$ { deny all; }。
如果资源走 CDN 或对象存储,问题更小:OSS 上的文件永远只是静态资源。
第三道:文件名与路径
- 重命名为随机名,绝不使用用户提供的文件名——它可能包含
../(路径穿越)或特殊字符; - 限制大小;
- 存储目录与 Web 根分离或至少不可枚举。
$stored = bin2hex(random_bytes(16)) . '.' . $ext;
第四道:内容层风险
- SVG 可以内嵌脚本,作为图片展示没问题,直接内联到页面会执行 XSS,要么拒绝要么净化;
- 图片可以携带多态 PHP(注释里的代码),所以「不可执行」必须真正生效;
- ZIP 解压要检查压缩包内文件名,防 Zip Slip。
上传接口本身
- 必须登录 + CSRF 防护;
- 限速(防刷满磁盘);
- 返回的 URL 不要直接拼接用户输入。
按这个清单逐项检查一遍,绝大多数上传漏洞都能在代码评审阶段被发现。最后一道保险是部署后的定期扫描:对上传目录做可疑特征扫描,及时清掉漏网的。