文件上传是 Web 应用中最常见的功能之一,头像、附件、证件照、导入表格,几乎每个系统都会用到。但正因为太常见,很多开发者只关注“能不能传上去”,却忽略了“传上去之后会发生什么”。文件上传是 Web 安全中危害等级极高的一类漏洞,一旦被利用,轻则存储型 XSS,重则直接获取服务器权限。
文件上传的常见攻击方式
1. 上传 WebShell 直接控制服务器
这是最直接、危害最大的利用方式。如果后端只检查了 Content-Type,或者只检查了文件扩展名黑名单,攻击者可以通过以下方式绕过:
- 修改 Content-Type 为
image/jpeg,实际内容仍是 PHP 代码 - 使用
.phtml、.php5、.pht等黑名单遗漏的扩展名 - 利用解析漏洞,如 Apache 的
.php.xxx、Nginx 的1.jpg/x.php - 在文件名中使用
%00截断(老版本 PHP) - 上传
.htaccess文件,让服务器把.jpg当 PHP 解析
一旦 WebShell 落地,攻击者可以执行任意命令、读取数据库配置、横向渗透内网。
2. 存储型 XSS
上传一个 SVG 或 HTML 文件,如果服务器直接以原 Content-Type 返回,浏览器会当作 HTML 解析,其中的 <script> 就会执行。SVG 文件尤其危险,它本质是 XML,可以内嵌 JavaScript,且很多系统允许上传 SVG 作为图片。
3. 文件包含与路径穿越
如果上传后的文件名由用户控制,攻击者可以构造 ../../../../etc/cron.d/evil 这样的路径,把文件写到系统关键目录。配合本地文件包含漏洞,还能直接执行上传的恶意文件。
4. 拒绝服务
上传超大文件占满磁盘,或者上传 zip 炸弹、图片炸弹(如“解压后 10GB”的 PNG),导致服务不可用。
后端加固的核心思路
防御文件上传,核心原则是:永远不要相信用户提供的任何数据,包括文件名、扩展名、Content-Type 和文件内容。
白名单校验扩展名
不要用黑名单。黑名单永远列不全,php3、php4、php5、phtml、pht、phar 层出不穷。正确做法是白名单:
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif', 'pdf', 'docx'}
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
注意要取最后一个点之后的部分,防止 evil.php.jpg 这类绕过。
校验文件真实类型
Content-Type 由客户端提供,可以随意伪造。应该用服务端工具检测文件头(Magic Number):
import imghdr
def is_real_image(path):
return imghdr.what(path) in ('jpeg', 'png', 'gif')
对于图片,更严格的做法是用 Pillow 重新编码,丢弃所有元数据和潜在的恶意载荷。
重命名文件
不要使用用户提供的文件名。用 UUID 或时间戳加随机数重命名,只保留白名单内的扩展名:
import uuid
new_name = f"{uuid.uuid4().hex}.{ext}"
这样既避免了路径穿越,也防止了 .htaccess、.user.ini 这类特殊文件名的攻击。
存储位置与权限
上传目录必须与 Web 根目录分离,或者至少禁止执行脚本。Nginx 配置示例:
location /uploads/ {
location ~ \.(php|phtml|php5|phar)$ {
deny all;
}
}
更彻底的做法是把文件存到对象存储(如 S3、OSS),Web 服务器只负责转发,根本不具备执行能力。
限制文件大小和数量
在 Nginx 和应用层同时限制 client_max_body_size,防止磁盘打满。对上传频率做限流,防止批量上传攻击。
前端能做什么
前端校验只是提升用户体验,不能替代后端校验,因为请求可以绕过浏览器直接发送。但前端可以做:
- 用
accept属性限制文件选择类型 - 在选择后立即检查扩展名和大小,给出友好提示
- 对图片做本地预览时,使用
URL.createObjectURL而非直接插入用户提供的 HTML
顺带说两句 XSS 与 SQL 注入
文件上传往往和 XSS、SQL 注入组合出现。比如上传的 SVG 触发 XSS 后,攻击者可以窃取管理员 Cookie,再通过 SQL 注入拖库。
XSS 的常见方式包括:反射型(URL 参数直接回显)、存储型(评论、昵称、上传文件)、DOM 型(innerHTML、document.write 拼接用户输入)。前端防御的核心是:输出时根据上下文转义,优先使用 textContent 而非 innerHTML,配合 CSP 限制脚本来源。但真正的防线仍在后端——所有用户输入在存储和输出时都要转义。
MySQL 防 SQL 注入,最有效的写法是参数化查询:
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
不要用字符串拼接,哪怕是 escape_string 也不够安全。ORM 如 SQLAlchemy、Django ORM 默认使用参数化,但要注意 .raw()、.extra() 这类逃逸口。此外,数据库账号遵循最小权限原则,Web 应用账号不应有 FILE、DROP 权限。
Linux 服务器安全加固清单
文件上传漏洞最终往往落到服务器上,基础加固能显著降低被拿 shell 后的影响:
- 及时更新系统和软件补丁,尤其是 Web 中间件
- 关闭不必要的端口和服务,
systemctl disable掉用不到的 - SSH 禁用 root 登录和密码登录,改用密钥 + 非标准端口
- 配置防火墙,只放行 80/443 和必要的管理端口
- Web 目录权限设为
www-data只读,上传目录禁止执行 - 开启
fail2ban防暴力破解 - 配置日志审计,监控异常文件写入和命令执行
- 定期用
lynis、rkhunter做安全检查
小结
文件上传的安全,本质上是“信任边界”的问题。前端的任何校验都只是体验优化,后端必须独立完成扩展名白名单、真实类型检测、重命名、存储隔离和权限控制。再配合 XSS 转义、参数化查询和服务器加固,才能把风险控制在可接受范围内。安全没有银弹,但每一层防御都会让攻击者的成本更高一点。
未经允许不得转载:任鹏个人博客 » 文件上传功能中的 Web 安全风险与加固


朋友圈点赞图在线生成源码