文件上传功能中的 Web 安全风险与加固

文件上传是 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 和文件内容。

白名单校验扩展名

不要用黑名单。黑名单永远列不全,php3php4php5phtmlphtphar 层出不穷。正确做法是白名单:

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 型(innerHTMLdocument.write 拼接用户输入)。前端防御的核心是:输出时根据上下文转义,优先使用 textContent 而非 innerHTML,配合 CSP 限制脚本来源。但真正的防线仍在后端——所有用户输入在存储和输出时都要转义。

MySQL 防 SQL 注入,最有效的写法是参数化查询:

cursor.execute("SELECT * FROM users WHERE name = %s", (name,))

不要用字符串拼接,哪怕是 escape_string 也不够安全。ORM 如 SQLAlchemy、Django ORM 默认使用参数化,但要注意 .raw().extra() 这类逃逸口。此外,数据库账号遵循最小权限原则,Web 应用账号不应有 FILEDROP 权限。

Linux 服务器安全加固清单

文件上传漏洞最终往往落到服务器上,基础加固能显著降低被拿 shell 后的影响:

  • 及时更新系统和软件补丁,尤其是 Web 中间件
  • 关闭不必要的端口和服务,systemctl disable 掉用不到的
  • SSH 禁用 root 登录和密码登录,改用密钥 + 非标准端口
  • 配置防火墙,只放行 80/443 和必要的管理端口
  • Web 目录权限设为 www-data 只读,上传目录禁止执行
  • 开启 fail2ban 防暴力破解
  • 配置日志审计,监控异常文件写入和命令执行
  • 定期用 lynisrkhunter 做安全检查

小结

文件上传的安全,本质上是“信任边界”的问题。前端的任何校验都只是体验优化,后端必须独立完成扩展名白名单、真实类型检测、重命名、存储隔离和权限控制。再配合 XSS 转义、参数化查询和服务器加固,才能把风险控制在可接受范围内。安全没有银弹,但每一层防御都会让攻击者的成本更高一点。

未经允许不得转载:任鹏个人博客 » 文件上传功能中的 Web 安全风险与加固

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏